Merge "Reduce the number of Font instance creation" into main
diff --git a/Android.bp b/Android.bp
index 54cb268..49a6a2b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -103,10 +103,10 @@
         ":android.hardware.gnss-V2-java-source",
         ":android.hardware.graphics.common-V3-java-source",
         ":android.hardware.keymaster-V4-java-source",
-        ":android.hardware.radio-V3-java-source",
-        ":android.hardware.radio.data-V3-java-source",
-        ":android.hardware.radio.network-V3-java-source",
-        ":android.hardware.radio.voice-V3-java-source",
+        ":android.hardware.radio-V4-java-source",
+        ":android.hardware.radio.data-V4-java-source",
+        ":android.hardware.radio.network-V4-java-source",
+        ":android.hardware.radio.voice-V4-java-source",
         ":android.hardware.security.secureclock-V1-java-source",
         ":android.hardware.thermal-V3-java-source",
         ":android.hardware.tv.tuner-V3-java-source",
@@ -232,13 +232,13 @@
         "android.hardware.gnss-V2.1-java",
         "android.hardware.health-V1.0-java-constants",
         "android.hardware.radio-V1.6-java",
-        "android.hardware.radio.data-V3-java",
-        "android.hardware.radio.ims-V2-java",
-        "android.hardware.radio.messaging-V3-java",
-        "android.hardware.radio.modem-V3-java",
-        "android.hardware.radio.network-V3-java",
-        "android.hardware.radio.sim-V3-java",
-        "android.hardware.radio.voice-V3-java",
+        "android.hardware.radio.data-V4-java",
+        "android.hardware.radio.ims-V3-java",
+        "android.hardware.radio.messaging-V4-java",
+        "android.hardware.radio.modem-V4-java",
+        "android.hardware.radio.network-V4-java",
+        "android.hardware.radio.sim-V4-java",
+        "android.hardware.radio.voice-V4-java",
         "android.hardware.thermal-V1.0-java-constants",
         "android.hardware.thermal-V1.0-java",
         "android.hardware.thermal-V1.1-java",
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 8bd3ef4..637c726 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -36,10 +36,14 @@
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.UidObserver;
+import android.app.compat.CompatChanges;
 import android.app.job.JobInfo;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
 import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.Overridable;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
@@ -132,6 +136,27 @@
         return (int) (val ^ (val >>> 32));
     }
 
+    /**
+     * When enabled this change id overrides the default quota policy enforcement to the jobs
+     * running in the foreground process state.
+     */
+    // TODO: b/379681266 - Might need some refactoring for a better app-compat strategy.
+    @VisibleForTesting
+    @ChangeId
+    @Disabled // Disabled by default
+    @Overridable // The change can be overridden in user build
+    static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS = 341201311L;
+
+    /**
+     * When enabled this change id overrides the default quota policy enforcement policy
+     * the jobs started when app was in the TOP state.
+     */
+    @VisibleForTesting
+    @ChangeId
+    @Disabled // Disabled by default
+    @Overridable // The change can be overridden in user build.
+    static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS = 374323858L;
+
     @VisibleForTesting
     static class ExecutionStats {
         /**
@@ -622,7 +647,9 @@
         }
 
         final int uid = jobStatus.getSourceUid();
-        if (!Flags.enforceQuotaPolicyToTopStartedJobs() && mTopAppCache.get(uid)) {
+        if ((!Flags.enforceQuotaPolicyToTopStartedJobs()
+                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
+                        uid)) && mTopAppCache.get(uid)) {
             if (DEBUG) {
                 Slog.d(TAG, jobStatus.toShortString() + " is top started job");
             }
@@ -659,7 +686,9 @@
                 timer.stopTrackingJob(jobStatus);
             }
         }
-        if (!Flags.enforceQuotaPolicyToTopStartedJobs()) {
+        if (!Flags.enforceQuotaPolicyToTopStartedJobs()
+                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
+                        jobStatus.getSourceUid())) {
             mTopStartedJobs.remove(jobStatus);
         }
     }
@@ -772,7 +801,13 @@
 
     /** @return true if the job was started while the app was in the TOP state. */
     private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
-        return !Flags.enforceQuotaPolicyToTopStartedJobs() && mTopStartedJobs.contains(jobStatus);
+        if (!Flags.enforceQuotaPolicyToTopStartedJobs()
+                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
+                        jobStatus.getSourceUid())) {
+            return mTopStartedJobs.contains(jobStatus);
+        }
+
+        return false;
     }
 
     /** Returns the maximum amount of time this job could run for. */
@@ -2634,9 +2669,13 @@
     }
 
     @VisibleForTesting
-    int getProcessStateQuotaFreeThreshold() {
-        return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP :
-                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+    int getProcessStateQuotaFreeThreshold(int uid) {
+        if (Flags.enforceQuotaPolicyToFgsJobs()
+                && !CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS, uid)) {
+            return ActivityManager.PROCESS_STATE_BOUND_TOP;
+        }
+
+        return ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
     }
 
     private class QcHandler extends Handler {
@@ -2776,7 +2815,7 @@
                                 isQuotaFree = true;
                             } else {
                                 final boolean reprocess;
-                                if (procState <= getProcessStateQuotaFreeThreshold()) {
+                                if (procState <= getProcessStateQuotaFreeThreshold(uid)) {
                                     reprocess = !mForegroundUids.get(uid);
                                     mForegroundUids.put(uid, true);
                                     isQuotaFree = true;
diff --git a/cmds/interrupter/interrupter.c b/cmds/interrupter/interrupter.c
index ae55515..8bb522a2 100644
--- a/cmds/interrupter/interrupter.c
+++ b/cmds/interrupter/interrupter.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#define _GNU_SOURCE
 
 /**
  * The probability of a syscall failing from 0.0 to 1.0
@@ -32,8 +33,6 @@
 #include <sys/stat.h>
 #include <fcntl.h>
 
-/* For builds on glibc */
-#define __USE_GNU
 #include <dlfcn.h>
 
 #include "interrupter.h"
diff --git a/core/api/current.txt b/core/api/current.txt
index 6cf572b..73d0d5b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -3830,7 +3830,7 @@
     method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public boolean notifyAccountAuthenticated(android.accounts.Account);
     method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public String peekAuthToken(android.accounts.Account, String);
     method @Deprecated @RequiresPermission(value="android.permission.MANAGE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<java.lang.Boolean> removeAccount(android.accounts.Account, android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler);
-    method @RequiresPermission(value="android.permission.MANAGE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<android.os.Bundle> removeAccount(android.accounts.Account, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
+    method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @RequiresPermission(value="android.permission.REMOVE_ACCOUNTS", conditional=true) public android.accounts.AccountManagerFuture<android.os.Bundle> removeAccount(android.accounts.Account, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
     method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public boolean removeAccountExplicitly(android.accounts.Account);
     method public void removeOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener);
     method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<android.accounts.Account> renameAccount(android.accounts.Account, @Size(min=1) String, android.accounts.AccountManagerCallback<android.accounts.Account>, android.os.Handler);
@@ -4614,6 +4614,7 @@
     method public void reportFullyDrawn();
     method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent);
     method public void requestFullscreenMode(int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
+    method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public final void requestOpenInBrowserEducation();
     method public final void requestPermissions(@NonNull String[], int);
     method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public final void requestPermissions(@NonNull String[], int, int);
     method public final void requestShowKeyboardShortcuts();
@@ -4639,7 +4640,6 @@
     method public void setInheritShowWhenLocked(boolean);
     method public void setIntent(android.content.Intent);
     method @FlaggedApi("android.security.content_uri_permission_apis") public void setIntent(@Nullable android.content.Intent, @Nullable android.app.ComponentCaller);
-    method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public final void setLimitSystemEducationDialogs(boolean);
     method public void setLocusContext(@Nullable android.content.LocusId, @Nullable android.os.Bundle);
     method public final void setMediaController(android.media.session.MediaController);
     method public void setPictureInPictureParams(@NonNull android.app.PictureInPictureParams);
@@ -13809,7 +13809,7 @@
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING, android.Manifest.permission.RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
     field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1
-    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100
+    field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS, android.health.connect.HealthPermissions.READ_HEART_RATE, android.health.connect.HealthPermissions.READ_SKIN_TEMPERATURE, android.health.connect.HealthPermissions.READ_OXYGEN_SATURATION}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8
     field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff
     field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2
@@ -21214,6 +21214,7 @@
     method public void setExtractView(android.view.View);
     method public void setExtractViewShown(boolean);
     method public void setInputView(android.view.View);
+    method @FlaggedApi("android.view.inputmethod.adaptive_handwriting_bounds") public final void setStylusHandwritingRegion(@NonNull android.graphics.Region);
     method public final void setStylusHandwritingSessionTimeout(@NonNull java.time.Duration);
     method public final boolean shouldOfferSwitchingToNextInputMethod();
     method public void showStatusIcon(@DrawableRes int);
@@ -34848,6 +34849,13 @@
     field public static final int EFFECT_TICK = 2; // 0x2
   }
 
+  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.BasicEnvelopeBuilder {
+    ctor public VibrationEffect.BasicEnvelopeBuilder();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0, to=1) float, long);
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder setInitialSharpness(@FloatRange(from=0, to=1) float);
+  }
+
   public static final class VibrationEffect.Composition {
     method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int);
     method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float);
@@ -34868,7 +34876,7 @@
 
   @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder {
     ctor public VibrationEffect.WaveformEnvelopeBuilder();
-    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int);
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, long);
     method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
     method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder setInitialFrequencyHz(@FloatRange(from=0) float);
   }
@@ -41180,6 +41188,18 @@
     method @Deprecated public void onSendTextSms(@NonNull String, int, @NonNull String, @NonNull android.service.carrier.CarrierMessagingService.ResultCallback<android.service.carrier.CarrierMessagingService.SendSmsResult>);
     method public void onSendTextSms(@NonNull String, int, @NonNull String, int, @NonNull android.service.carrier.CarrierMessagingService.ResultCallback<android.service.carrier.CarrierMessagingService.SendSmsResult>);
     field public static final int DOWNLOAD_STATUS_ERROR = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 606; // 0x25e
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED = 610; // 0x262
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE = 603; // 0x25b
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 609; // 0x261
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN = 601; // 0x259
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 608; // 0x260
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR = 604; // 0x25c
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 611; // 0x263
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK = 607; // 0x25f
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_RETRY = 605; // 0x25d
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 602; // 0x25a
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED = 600; // 0x258
     field public static final int DOWNLOAD_STATUS_OK = 0; // 0x0
     field public static final int DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK = 1; // 0x1
     field public static final int RECEIVE_OPTIONS_DEFAULT = 0; // 0x0
@@ -41187,7 +41207,38 @@
     field public static final int RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE = 2; // 0x2
     field public static final int SEND_FLAG_REQUEST_DELIVERY_STATUS = 1; // 0x1
     field public static final int SEND_STATUS_ERROR = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 406; // 0x196
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_DATA_DISABLED = 410; // 0x19a
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_HTTP_FAILURE = 403; // 0x193
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 409; // 0x199
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INVALID_APN = 401; // 0x191
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 408; // 0x198
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_IO_ERROR = 404; // 0x194
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 411; // 0x19b
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK = 407; // 0x197
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_RETRY = 405; // 0x195
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 402; // 0x192
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_UNSPECIFIED = 400; // 0x190
     field public static final int SEND_STATUS_OK = 0; // 0x0
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_CANCELLED = 215; // 0xd7
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ENCODING_ERROR = 212; // 0xd4
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE = 204; // 0xcc
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE = 200; // 0xc8
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED = 203; // 0xcb
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_NO_SERVICE = 202; // 0xca
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_NULL_PDU = 201; // 0xc9
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED = 206; // 0xce
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED = 205; // 0xcd
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_ARGUMENTS = 208; // 0xd0
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS = 213; // 0xd5
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_SMS_FORMAT = 210; // 0xd2
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_STATE = 209; // 0xd1
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_NETWORK_ERROR = 211; // 0xd3
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_NETWORK_REJECT = 207; // 0xcf
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED = 214; // 0xd6
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED = 216; // 0xd8
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY = 217; // 0xd9
+    field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED = 218; // 0xda
     field public static final int SEND_STATUS_RETRY_ON_CARRIER_NETWORK = 1; // 0x1
     field public static final String SERVICE_INTERFACE = "android.service.carrier.CarrierMessagingService";
   }
@@ -53362,6 +53413,7 @@
     method @NonNull public android.view.SurfaceControl.Transaction setBuffer(@NonNull android.view.SurfaceControl, @Nullable android.hardware.HardwareBuffer, @Nullable android.hardware.SyncFence, @Nullable java.util.function.Consumer<android.hardware.SyncFence>);
     method @NonNull public android.view.SurfaceControl.Transaction setBufferSize(@NonNull android.view.SurfaceControl, @IntRange(from=0) int, @IntRange(from=0) int);
     method @NonNull public android.view.SurfaceControl.Transaction setBufferTransform(@NonNull android.view.SurfaceControl, int);
+    method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public android.view.SurfaceControl.Transaction setContentPriority(@NonNull android.view.SurfaceControl, int);
     method @NonNull public android.view.SurfaceControl.Transaction setCrop(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect);
     method @NonNull public android.view.SurfaceControl.Transaction setDamageRegion(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Region);
     method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bc0037d..a0d10f5 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -66,10 +66,10 @@
     field @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public static final String BIND_EXPLICIT_HEALTH_CHECK_SERVICE = "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE";
     field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
     field public static final String BIND_FIELD_CLASSIFICATION_SERVICE = "android.permission.BIND_FIELD_CLASSIFICATION_SERVICE";
-    field @FlaggedApi("android.security.afl_api") public static final String BIND_FORENSIC_EVENT_TRANSPORT_SERVICE = "android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE";
     field public static final String BIND_GBA_SERVICE = "android.permission.BIND_GBA_SERVICE";
     field public static final String BIND_HOTWORD_DETECTION_SERVICE = "android.permission.BIND_HOTWORD_DETECTION_SERVICE";
     field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE";
+    field @FlaggedApi("android.security.afl_api") public static final String BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE = "android.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE";
     field public static final String BIND_KEYGUARD_APPWIDGET = "android.permission.BIND_KEYGUARD_APPWIDGET";
     field public static final String BIND_MUSIC_RECOGNITION_SERVICE = "android.permission.BIND_MUSIC_RECOGNITION_SERVICE";
     field public static final String BIND_NETWORK_RECOMMENDATION_SERVICE = "android.permission.BIND_NETWORK_RECOMMENDATION_SERVICE";
@@ -103,6 +103,7 @@
     field public static final String BRIGHTNESS_SLIDER_USAGE = "android.permission.BRIGHTNESS_SLIDER_USAGE";
     field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS";
     field @Deprecated public static final String BROADCAST_NETWORK_PRIVILEGED = "android.permission.BROADCAST_NETWORK_PRIVILEGED";
+    field @FlaggedApi("android.media.audio.concurrent_audio_record_bypass_permission") public static final String BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION = "android.permission.BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION";
     field public static final String BYPASS_ROLE_QUALIFICATION = "android.permission.BYPASS_ROLE_QUALIFICATION";
     field public static final String CALL_AUDIO_INTERCEPTION = "android.permission.CALL_AUDIO_INTERCEPTION";
     field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED";
@@ -134,6 +135,7 @@
     field public static final String CONTROL_KEYGUARD_SECURE_NOTIFICATIONS = "android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS";
     field public static final String CONTROL_OEM_PAID_NETWORK_PREFERENCE = "android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE";
     field public static final String CONTROL_VPN = "android.permission.CONTROL_VPN";
+    field @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") public static final String COPY_ACCOUNTS = "android.permission.COPY_ACCOUNTS";
     field public static final String CREATE_USERS = "android.permission.CREATE_USERS";
     field public static final String CREATE_VIRTUAL_DEVICE = "android.permission.CREATE_VIRTUAL_DEVICE";
     field public static final String CRYPT_KEEPER = "android.permission.CRYPT_KEEPER";
@@ -212,12 +214,12 @@
     field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES";
     field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
     field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
-    field @FlaggedApi("android.security.afl_api") public static final String MANAGE_FORENSIC_STATE = "android.permission.MANAGE_FORENSIC_STATE";
     field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
     field public static final String MANAGE_GAME_MODE = "android.permission.MANAGE_GAME_MODE";
     field @FlaggedApi("android.media.tv.flags.media_quality_fw") public static final String MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE = "android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE";
     field @FlaggedApi("android.media.tv.flags.media_quality_fw") public static final String MANAGE_GLOBAL_SOUND_QUALITY_SERVICE = "android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE";
     field public static final String MANAGE_HOTWORD_DETECTION = "android.permission.MANAGE_HOTWORD_DETECTION";
+    field @FlaggedApi("android.security.afl_api") public static final String MANAGE_INTRUSION_DETECTION_STATE = "android.permission.MANAGE_INTRUSION_DETECTION_STATE";
     field public static final String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS";
     field public static final String MANAGE_LOW_POWER_STANDBY = "android.permission.MANAGE_LOW_POWER_STANDBY";
     field public static final String MANAGE_MUSIC_RECOGNITION = "android.permission.MANAGE_MUSIC_RECOGNITION";
@@ -229,6 +231,7 @@
     field public static final String MANAGE_ROTATION_RESOLVER = "android.permission.MANAGE_ROTATION_RESOLVER";
     field public static final String MANAGE_SAFETY_CENTER = "android.permission.MANAGE_SAFETY_CENTER";
     field public static final String MANAGE_SEARCH_UI = "android.permission.MANAGE_SEARCH_UI";
+    field @FlaggedApi("android.security.secure_lockdown") public static final String MANAGE_SECURE_LOCK_DEVICE = "android.permission.MANAGE_SECURE_LOCK_DEVICE";
     field public static final String MANAGE_SENSOR_PRIVACY = "android.permission.MANAGE_SENSOR_PRIVACY";
     field public static final String MANAGE_SMARTSPACE = "android.permission.MANAGE_SMARTSPACE";
     field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER";
@@ -308,10 +311,10 @@
     field public static final String READ_CONTENT_RATING_SYSTEMS = "android.permission.READ_CONTENT_RATING_SYSTEMS";
     field public static final String READ_DEVICE_CONFIG = "android.permission.READ_DEVICE_CONFIG";
     field public static final String READ_DREAM_STATE = "android.permission.READ_DREAM_STATE";
-    field @FlaggedApi("android.security.afl_api") public static final String READ_FORENSIC_STATE = "android.permission.READ_FORENSIC_STATE";
     field public static final String READ_GLOBAL_APP_SEARCH_DATA = "android.permission.READ_GLOBAL_APP_SEARCH_DATA";
     field @FlaggedApi("android.content.pm.get_resolved_apk_path") public static final String READ_INSTALLED_SESSION_PATHS = "android.permission.READ_INSTALLED_SESSION_PATHS";
     field public static final String READ_INSTALL_SESSIONS = "android.permission.READ_INSTALL_SESSIONS";
+    field @FlaggedApi("android.security.afl_api") public static final String READ_INTRUSION_DETECTION_STATE = "android.permission.READ_INTRUSION_DETECTION_STATE";
     field public static final String READ_NETWORK_USAGE_HISTORY = "android.permission.READ_NETWORK_USAGE_HISTORY";
     field public static final String READ_OEM_UNLOCK_STATE = "android.permission.READ_OEM_UNLOCK_STATE";
     field public static final String READ_PEOPLE_DATA = "android.permission.READ_PEOPLE_DATA";
@@ -345,6 +348,7 @@
     field public static final String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION";
     field public static final String REGISTER_STATS_PULL_ATOM = "android.permission.REGISTER_STATS_PULL_ATOM";
     field public static final String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER";
+    field @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") public static final String REMOVE_ACCOUNTS = "android.permission.REMOVE_ACCOUNTS";
     field public static final String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES";
     field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
     field public static final String RENOUNCE_PERMISSIONS = "android.permission.RENOUNCE_PERMISSIONS";
@@ -570,6 +574,7 @@
 package android.accounts {
 
   public class AccountManager {
+    method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>, @Nullable android.os.Handler);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.accounts.AccountManagerFuture<android.os.Bundle> finishSessionAsUser(android.os.Bundle, android.app.Activity, android.os.UserHandle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
   }
 
@@ -3869,6 +3874,7 @@
     field public static final String APP_INTEGRITY_SERVICE = "app_integrity";
     field public static final String APP_PREDICTION_SERVICE = "app_prediction";
     field public static final String AUDIO_DEVICE_VOLUME_SERVICE = "audio_device_volume";
+    field @FlaggedApi("android.security.secure_lockdown") public static final String AUTHENTICATION_POLICY_SERVICE = "authentication_policy";
     field public static final String BACKUP_SERVICE = "backup";
     field public static final String BATTERY_STATS_SERVICE = "batterystats";
     field public static final int BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS = 1048576; // 0x100000
@@ -8165,6 +8171,14 @@
     method @NonNull public android.media.quality.PictureProfile.Builder setProfileType(int);
   }
 
+  @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public final class PictureProfileHandle implements android.os.Parcelable {
+    method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public int describeContents();
+    method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public long getId();
+    method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public static final android.os.Parcelable.Creator<android.media.quality.PictureProfileHandle> CREATOR;
+    field @NonNull public static final android.media.quality.PictureProfileHandle NONE;
+  }
+
 }
 
 package android.media.session {
@@ -9893,7 +9907,7 @@
     method public int getSignalStrength();
     method public int getSnr();
     method public int getSpectralInversion();
-    method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExt getStandardExt();
+    method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExtension getStandardExtension();
     method @NonNull public int[] getStreamIds();
     method public int getSymbolRate();
     method @IntRange(from=0, to=65535) public int getSystemId();
@@ -9948,7 +9962,7 @@
     field public static final int FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH = 6; // 0x6
     field public static final int FRONTEND_STATUS_TYPE_SNR = 1; // 0x1
     field public static final int FRONTEND_STATUS_TYPE_SPECTRAL = 10; // 0xa
-    field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = 47; // 0x2f
+    field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXTENSION = 47; // 0x2f
     field public static final int FRONTEND_STATUS_TYPE_STREAM_IDS = 39; // 0x27
     field public static final int FRONTEND_STATUS_TYPE_SYMBOL_RATE = 7; // 0x7
     field public static final int FRONTEND_STATUS_TYPE_T2_SYSTEM_ID = 29; // 0x1d
@@ -10240,9 +10254,9 @@
     method public default void onUnlocked();
   }
 
-  @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExt {
-    method public int getDvbsStandardExt();
-    method public int getDvbtStandardExt();
+  @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExtension {
+    method public int getDvbsStandardExtension();
+    method public int getDvbtStandardExtension();
   }
 
 }
@@ -12858,13 +12872,43 @@
 
 }
 
-package android.security.forensic {
+package android.security.authenticationpolicy {
 
-  @FlaggedApi("android.security.afl_api") public class ForensicManager {
-    method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void addStateCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void disable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void enable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback);
-    method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void removeStateCallback(@NonNull java.util.function.Consumer<java.lang.Integer>);
+  @FlaggedApi("android.security.secure_lockdown") public final class AuthenticationPolicyManager {
+    method @FlaggedApi("android.security.secure_lockdown") @RequiresPermission(android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE) public int disableSecureLockDevice(@NonNull android.security.authenticationpolicy.DisableSecureLockDeviceParams);
+    method @FlaggedApi("android.security.secure_lockdown") @RequiresPermission(android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE) public int enableSecureLockDevice(@NonNull android.security.authenticationpolicy.EnableSecureLockDeviceParams);
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_ALREADY_ENABLED = 6; // 0x6
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_INSUFFICIENT_BIOMETRICS = 5; // 0x5
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_INVALID_PARAMS = 3; // 0x3
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_NO_BIOMETRICS_ENROLLED = 4; // 0x4
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_UNKNOWN = 0; // 0x0
+    field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_UNSUPPORTED = 2; // 0x2
+    field @FlaggedApi("android.security.secure_lockdown") public static final int SUCCESS = 1; // 0x1
+  }
+
+  @FlaggedApi("android.security.secure_lockdown") public final class DisableSecureLockDeviceParams implements android.os.Parcelable {
+    ctor public DisableSecureLockDeviceParams(@NonNull String);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.security.authenticationpolicy.DisableSecureLockDeviceParams> CREATOR;
+  }
+
+  @FlaggedApi("android.security.secure_lockdown") public final class EnableSecureLockDeviceParams implements android.os.Parcelable {
+    ctor public EnableSecureLockDeviceParams(@NonNull String);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.security.authenticationpolicy.EnableSecureLockDeviceParams> CREATOR;
+  }
+
+}
+
+package android.security.intrusiondetection {
+
+  @FlaggedApi("android.security.afl_api") public class IntrusionDetectionManager {
+    method @RequiresPermission(android.Manifest.permission.READ_INTRUSION_DETECTION_STATE) public void addStateCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE) public void disable(@NonNull java.util.concurrent.Executor, @NonNull android.security.intrusiondetection.IntrusionDetectionManager.CommandCallback);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE) public void enable(@NonNull java.util.concurrent.Executor, @NonNull android.security.intrusiondetection.IntrusionDetectionManager.CommandCallback);
+    method @RequiresPermission(android.Manifest.permission.READ_INTRUSION_DETECTION_STATE) public void removeStateCallback(@NonNull java.util.function.Consumer<java.lang.Integer>);
     field public static final int ERROR_DATA_SOURCE_UNAVAILABLE = 4; // 0x4
     field public static final int ERROR_PERMISSION_DENIED = 1; // 0x1
     field public static final int ERROR_TRANSPORT_UNAVAILABLE = 3; // 0x3
@@ -12874,7 +12918,7 @@
     field public static final int STATE_UNKNOWN = 0; // 0x0
   }
 
-  public static interface ForensicManager.CommandCallback {
+  public static interface IntrusionDetectionManager.CommandCallback {
     method public void onFailure(int);
     method public void onSuccess();
   }
@@ -18963,6 +19007,10 @@
 
 package android.view {
 
+  public static class SurfaceControl.Transaction implements java.io.Closeable android.os.Parcelable {
+    method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public android.view.SurfaceControl.Transaction setPictureProfileHandle(@NonNull android.view.SurfaceControl, @NonNull android.media.quality.PictureProfileHandle);
+  }
+
   @UiThread public class View implements android.view.accessibility.AccessibilityEventSource android.graphics.drawable.Drawable.Callback android.view.KeyEvent.Callback {
     method @NonNull public final java.util.List<android.graphics.Rect> getUnrestrictedPreferKeepClearRects();
     method @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) public final void setUnrestrictedPreferKeepClearRects(@NonNull java.util.List<android.graphics.Rect>);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 44bcc2a..0771d4c 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1805,6 +1805,7 @@
     method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
     method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByDescriptor(@NonNull String);
     method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByPort(@NonNull String);
+    method public void resetLockedModifierState();
     field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL
   }
 
@@ -2492,6 +2493,7 @@
     method public boolean areAutoPowerSaveModesEnabled();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_LOW_POWER_STANDBY, android.Manifest.permission.DEVICE_POWER}) public void forceLowPowerStandbyActive(boolean);
     method @FlaggedApi("android.os.battery_saver_supported_check_api") public boolean isBatterySaverSupported();
+    method public boolean isInteractive(int);
     field public static final String ACTION_ENHANCED_DISCHARGE_PREDICTION_CHANGED = "android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED";
     field @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public static final int SYSTEM_WAKELOCK = -2147483648; // 0x80000000
   }
@@ -2781,6 +2783,17 @@
 
 package android.os.vibrator {
 
+  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class BasicPwleSegment extends android.os.vibrator.VibrationEffectSegment {
+    method public int describeContents();
+    method public long getDuration();
+    method public float getEndIntensity();
+    method public float getEndSharpness();
+    method public float getStartIntensity();
+    method public float getStartSharpness();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.BasicPwleSegment> CREATOR;
+  }
+
   public final class PrebakedSegment extends android.os.vibrator.VibrationEffectSegment {
     method public int describeContents();
     method public long getDuration();
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index c2fac70..9140bdf 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -2139,6 +2139,8 @@
     New API must be flagged with @FlaggedApi: field android.os.BugreportParams.BUGREPORT_MODE_MAX_VALUE
 UnflaggedApi: android.os.PowerManager#isBatterySaverSupported():
     New API must be flagged with @FlaggedApi: method android.os.PowerManager.isBatterySaverSupported()
+UnflaggedApi: android.os.PowerManager#isInteractive(int):
+    New API must be flagged with @FlaggedApi: method android.os.PowerManager.isInteractive(int)
 UnflaggedApi: android.os.UserHandle#USER_CURRENT:
     New API must be flagged with @FlaggedApi: field android.os.UserHandle.USER_CURRENT
 UnflaggedApi: android.os.UserManager#getAliveUsers():
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 87acbbf..72450999 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -16,9 +16,13 @@
 
 package android.accounts;
 
+import static android.Manifest.permission.COPY_ACCOUNTS;
+import static android.Manifest.permission.REMOVE_ACCOUNTS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED;
 
 import android.annotation.BroadcastBehavior;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +30,7 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.Size;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
@@ -1312,7 +1317,8 @@
      * {@link AccountManagerFuture} must not be used on the main thread.
      *
      * <p>This method requires the caller to have a signature match with the
-     * authenticator that manages the specified account.
+     * authenticator that manages the specified account, be a profile owner or have the
+     * {@link android.Manifest.permission#REMOVE_ACCOUNTS} permission.
      *
      * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
      * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
@@ -1344,6 +1350,8 @@
      * </ul>
      */
     @UserHandleAware
+    @RequiresPermission(value = REMOVE_ACCOUNTS, conditional = true)
+    @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED)
     public AccountManagerFuture<Bundle> removeAccount(final Account account,
             final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
         return removeAccountAsUser(account, activity, callback, handler, mContext.getUser());
@@ -2019,9 +2027,15 @@
      * succeeded.
      * @hide
      */
+    @SuppressLint("SamShouldBeLast")
+    @NonNull
+    @SystemApi
+    @RequiresPermission(anyOf = {COPY_ACCOUNTS, INTERACT_ACROSS_USERS_FULL})
+    @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED)
     public AccountManagerFuture<Boolean> copyAccountToUser(
-            final Account account, final UserHandle fromUser, final UserHandle toUser,
-            AccountManagerCallback<Boolean> callback, Handler handler) {
+            @NonNull final Account account, @NonNull final UserHandle fromUser,
+            @NonNull final UserHandle toUser, @Nullable AccountManagerCallback<Boolean> callback,
+            @Nullable Handler handler) {
         if (account == null) throw new IllegalArgumentException("account is null");
         if (toUser == null || fromUser == null) {
             throw new IllegalArgumentException("fromUser and toUser cannot be null");
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 419eb7d..38aea64 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1270,27 +1270,22 @@
     }
 
     /**
-     * To make users aware of system features such as the app header menu and its various
-     * functionalities, educational dialogs are shown to demonstrate how to find and utilize these
-     * features. Using this method, an activity can specify if it wants these educational dialogs to
-     * be shown. When set to {@code true}, these dialogs are not completely blocked; however, the
-     * system will be notified that they should not be shown unless necessary. If this API is not
-     * called, the system's educational dialogs are not limited by default.
+     * Requests to show the “Open in browser” education. “Open in browser” is a feature
+     * within the app header that allows users to switch from an app to the web. The feature
+     * is made available when an application is opened by a user clicking a link or when a
+     * link is provided by an application. Links can be provided by utilizing
+     * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+     * {@link AssistContent#setWebUri}.
      *
-     * <p>This method can be utilized when activities have states where showing an
-     * educational dialog would be disruptive to the user. For example, if a game application is
-     * expecting prompt user input, this method can be used to limit educational dialogs such as the
-     * dialogs that showcase the app header's features which, in this instance, would disrupt the
-     * user's experience if shown.</p>
-     *
-     * <p>Note that educational dialogs may be shown soon after this activity is launched, so
-     * this method must be called early if the intent is to limit the dialogs from the start.</p>
+     * <p>This method should be utilized when an activity wants to nudge the user to switch
+     * to the web application in cases where the web may provide the user with a better
+     * experience. Note that this method does not guarantee that the education will be shown.</p>
      */
     @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
-    public final void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) {
+    public final void requestOpenInBrowserEducation() {
         try {
             ActivityTaskManager
-                  .getService().setLimitSystemEducationDialogs(mToken, limitSystemEducationDialogs);
+                  .getService().requestOpenInBrowserEducation(mToken);
         } catch (RemoteException e) {
             // Empty
         }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index ab75069..33ba058 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -2679,62 +2679,6 @@
      */
     public static class RecentTaskInfo extends TaskInfo implements Parcelable {
         /**
-         * @hide
-         */
-        public static class PersistedTaskSnapshotData {
-            /**
-             * The bounds of the task when the last snapshot was taken, may be null if the task is
-             * not yet attached to the hierarchy.
-             * @see {@link android.window.TaskSnapshot#mTaskSize}.
-             * @hide
-             */
-            public @Nullable Point taskSize;
-
-            /**
-             * The content insets of the task when the task snapshot was taken.
-             * @see {@link android.window.TaskSnapshot#mContentInsets}.
-             * @hide
-             */
-            public @Nullable Rect contentInsets;
-
-            /**
-             * The size of the last snapshot taken, may be null if there is no associated snapshot.
-             * @see {@link android.window.TaskSnapshot#mSnapshot}.
-             * @hide
-             */
-            public @Nullable Point bufferSize;
-
-            /**
-             * Sets the data from the other data.
-             * @hide
-             */
-            public void set(PersistedTaskSnapshotData other) {
-                taskSize = other.taskSize;
-                contentInsets = other.contentInsets;
-                bufferSize = other.bufferSize;
-            }
-
-            /**
-             * Sets the data from the provided {@param snapshot}.
-             * @hide
-             */
-            public void set(TaskSnapshot snapshot) {
-                if (snapshot == null) {
-                    taskSize = null;
-                    contentInsets = null;
-                    bufferSize = null;
-                    return;
-                }
-                final HardwareBuffer buffer = snapshot.getHardwareBuffer();
-                taskSize = new Point(snapshot.getTaskSize());
-                contentInsets = new Rect(snapshot.getContentInsets());
-                bufferSize = buffer != null
-                        ? new Point(buffer.getWidth(), buffer.getHeight())
-                        : null;
-            }
-        }
-
-        /**
          * If this task is currently running, this is the identifier for it.
          * If it is not running, this will be -1.
          *
@@ -2770,24 +2714,6 @@
         @Deprecated
         public int affiliatedTaskId;
 
-        /**
-         * Information of organized child tasks.
-         *
-         * @deprecated No longer used
-         * @hide
-         */
-        @Deprecated
-        public ArrayList<RecentTaskInfo> childrenTaskInfos = new ArrayList<>();
-
-        /**
-         * Information about the last snapshot taken for this task.
-         *
-         * @deprecated No longer used
-         * @hide
-         */
-        @Deprecated
-        public PersistedTaskSnapshotData lastSnapshotData = new PersistedTaskSnapshotData();
-
         public RecentTaskInfo() {
         }
 
@@ -2803,10 +2729,6 @@
         public void readFromParcel(Parcel source) {
             id = source.readInt();
             persistentId = source.readInt();
-            childrenTaskInfos = source.readArrayList(RecentTaskInfo.class.getClassLoader(), android.app.ActivityManager.RecentTaskInfo.class);
-            lastSnapshotData.taskSize = source.readTypedObject(Point.CREATOR);
-            lastSnapshotData.contentInsets = source.readTypedObject(Rect.CREATOR);
-            lastSnapshotData.bufferSize = source.readTypedObject(Point.CREATOR);
             super.readTaskFromParcel(source);
         }
 
@@ -2814,10 +2736,6 @@
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeInt(id);
             dest.writeInt(persistentId);
-            dest.writeList(childrenTaskInfos);
-            dest.writeTypedObject(lastSnapshotData.taskSize, flags);
-            dest.writeTypedObject(lastSnapshotData.contentInsets, flags);
-            dest.writeTypedObject(lastSnapshotData.bufferSize, flags);
             super.writeTaskToParcel(dest, flags);
         }
 
@@ -2884,11 +2802,6 @@
                 pw.println(" }");
             }
             pw.print("   ");
-            pw.print(" lastSnapshotData {");
-            pw.print(" taskSize=" + lastSnapshotData.taskSize);
-            pw.print(" contentInsets=" + lastSnapshotData.contentInsets);
-            pw.print(" bufferSize=" + lastSnapshotData.bufferSize);
-            pw.println(" }");
         }
     }
 
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index f80121d..d9f8d33 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1150,6 +1150,20 @@
     public abstract void stopForegroundServiceDelegate(@NonNull ServiceConnection connection);
 
     /**
+     * Notifies that a media foreground service associated with a media session has
+     * transitioned to a "user-disengaged" state.
+     * Upon receiving this notification, service may be removed from the foreground state. It
+     * should only be called by {@link com.android.server.media.MediaSessionService}
+     *
+     * @param packageName The package name of the app running the media foreground service.
+     * @param userId The user ID associated with the foreground service.
+     * @param notificationId The ID of the media notification associated with the foreground
+     *                      service.
+     */
+    public abstract void notifyInactiveMediaForegroundService(@NonNull String packageName,
+            @UserIdInt int userId, int notificationId);
+
+    /**
      * Same as {@link android.app.IActivityManager#startProfile(int userId)}, but it would succeed
      * even if the profile is disabled - it should only be called by
      * {@link com.android.server.devicepolicy.DevicePolicyManagerService} when starting a profile
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index 16444dc..6efc4ef 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -62,6 +62,7 @@
 import android.hardware.usb.UsbAccessory;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbManager;
+import android.health.connect.HealthPermissions;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -484,21 +485,35 @@
      */
     public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_HEALTH =
             new ForegroundServiceTypePolicyInfo(
-            FOREGROUND_SERVICE_TYPE_HEALTH,
-            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
-            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
-            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
-                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH)
-            }, true),
-            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
-                new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION),
-                new RegularPermission(Manifest.permission.BODY_SENSORS),
-                new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS),
-            }, false),
-            FGS_TYPE_PERM_ENFORCEMENT_FLAG_HEALTH /* permissionEnforcementFlag */,
-            true /* permissionEnforcementFlagDefaultValue */,
-            false /* foregroundOnlyPermission */
-    );
+                    FOREGROUND_SERVICE_TYPE_HEALTH,
+                    ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+                    ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+                    new ForegroundServiceTypePermissions(
+                            new ForegroundServiceTypePermission[] {
+                                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH)
+                            },
+                            true),
+                    new ForegroundServiceTypePermissions(getAllowedHealthPermissions(), false),
+                    FGS_TYPE_PERM_ENFORCEMENT_FLAG_HEALTH /* permissionEnforcementFlag */,
+                    true /* permissionEnforcementFlagDefaultValue */,
+                    false /* foregroundOnlyPermission */);
+
+    /** Returns the permissions needed for the policy of the health foreground service type. */
+    private static ForegroundServiceTypePermission[] getAllowedHealthPermissions() {
+        final ArrayList<ForegroundServiceTypePermission> permissions = new ArrayList<>();
+        permissions.add(new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION));
+        permissions.add(new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS));
+
+        if (android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) {
+            permissions.add(new RegularPermission(HealthPermissions.READ_HEART_RATE));
+            permissions.add(new RegularPermission(HealthPermissions.READ_SKIN_TEMPERATURE));
+            permissions.add(new RegularPermission(HealthPermissions.READ_OXYGEN_SATURATION));
+        } else {
+            permissions.add(new RegularPermission(Manifest.permission.BODY_SENSORS));
+        }
+
+        return permissions.toArray(new ForegroundServiceTypePermission[permissions.size()]);
+    }
 
     /**
      * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}.
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index ec7b72e..c6f62a2 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -242,8 +242,8 @@
 
     boolean supportsLocalVoiceInteraction();
 
-    // Sets whether system educational dialogs should be limited
-    void setLimitSystemEducationDialogs(IBinder appToken, boolean limitSystemEducationDialogs);
+    // Requests the "Open in browser" education to be shown
+    void requestOpenInBrowserEducation(IBinder appToken);
 
     // Get device configuration
     ConfigurationInfo getDeviceConfigurationInfo();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index eb161ea..b78f111 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -814,8 +814,8 @@
         if (Flags.notificationsRedesignTemplates()) {
             return switch (layoutId) {
                 case R.layout.notification_2025_template_collapsed_base,
+                     R.layout.notification_2025_template_heads_up_base,
                      R.layout.notification_2025_template_header,
-                     R.layout.notification_template_material_heads_up_base,
                      R.layout.notification_template_material_big_base,
                      R.layout.notification_template_material_big_picture,
                      R.layout.notification_template_material_big_text,
@@ -3257,7 +3257,7 @@
      */
     @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING)
     public boolean hasPromotableCharacteristics() {
-        return isColorized()
+        return isColorizedRequested()
                 && hasTitle()
                 && !containsCustomViews()
                 && hasPromotableStyle();
@@ -4083,6 +4083,12 @@
                 flags &= ~FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
             }
         }
+        if (Flags.apiRichOngoing()) {
+            if ((flags & FLAG_PROMOTED_ONGOING) != 0) {
+                flagStrings.add("PROMOTED_ONGOING");
+                flags &= ~FLAG_PROMOTED_ONGOING;
+            }
+        }
 
         if (android.service.notification.Flags.notificationSilentFlag()) {
             if ((flags & FLAG_SILENT) != 0) {
@@ -7517,7 +7523,11 @@
         }
 
         private int getHeadsUpBaseLayoutResource() {
-            return R.layout.notification_template_material_heads_up_base;
+            if (Flags.notificationsRedesignTemplates()) {
+                return R.layout.notification_2025_template_heads_up_base;
+            } else {
+                return R.layout.notification_template_material_heads_up_base;
+            }
         }
 
         private int getCompactHeadsUpBaseLayoutResource() {
@@ -7788,8 +7798,16 @@
      * @hide
      */
     public boolean isColorized() {
-        return extras.getBoolean(EXTRA_COLORIZED)
-                && (hasColorizedPermission() || isFgsOrUij());
+        return isColorizedRequested()
+                && (hasColorizedPermission() || isFgsOrUij() || isPromotedOngoing());
+    }
+
+    /**
+     * @return true if this notification has requested to be colorized, regardless of whether it
+     * meets the requirements to be displayed that way.
+     */
+    private boolean isColorizedRequested() {
+        return extras.getBoolean(EXTRA_COLORIZED);
     }
 
     /**
@@ -7803,6 +7821,19 @@
     }
 
     /**
+     * Returns whether this notification is a promoted ongoing notification.
+     *
+     * This requires the Notification.FLAG_PROMOTED_ONGOING flag to be set
+     * (which may be true once the api_rich_ongoing feature flag is enabled),
+     * and requires that the ui_rich_ongoing feature flag is enabled.
+     *
+     * @hide
+     */
+    public boolean isPromotedOngoing() {
+        return Flags.uiRichOngoing() && (flags & Notification.FLAG_PROMOTED_ONGOING) != 0;
+    }
+
+    /**
      * @return true if this is a media style notification with a media session
      *
      * @hide
@@ -11667,8 +11698,10 @@
             return points;
         }
 
-        @NonNull
-        private NotificationProgressModel createProgressModel(int defaultProgressColor,
+        /**
+         * @hide
+         */
+        public @NonNull NotificationProgressModel createProgressModel(int defaultProgressColor,
                 int backgroundColor) {
             final NotificationProgressModel model;
             if (mIndeterminate) {
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 087e246..3cffca7 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -206,11 +206,15 @@
             assets.addPresetApkKeys(extractApkKeys(collector.collectedKey()));
             return new Pair<>(assets, size);
         }
-        final var newAssetsBuilder = new AssetManager.Builder();
+        final var newAssetsBuilder = new AssetManager.Builder().setNoInit();
         for (final var asset : assets.getApkAssets()) {
-            if (!asset.isForLoader()) {
-                newAssetsBuilder.addApkAssets(asset);
+            // Skip everything that's either default, or will get added by the collector (builder
+            // doesn't check for duplicates at all).
+            if (asset.isSystem() || asset.isForLoader() || asset.isOverlay()
+                    || asset.isSharedLib()) {
+                continue;
             }
+            newAssetsBuilder.addApkAssets(asset);
         }
         for (final var key : extractApkKeys(collector.collectedKey())) {
             try {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 53a7dad..2bd2d34 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -241,8 +241,10 @@
 import android.security.advancedprotection.IAdvancedProtectionService;
 import android.security.attestationverification.AttestationVerificationManager;
 import android.security.attestationverification.IAttestationVerificationManagerService;
-import android.security.forensic.ForensicManager;
-import android.security.forensic.IForensicService;
+import android.security.authenticationpolicy.AuthenticationPolicyManager;
+import android.security.authenticationpolicy.IAuthenticationPolicyService;
+import android.security.intrusiondetection.IIntrusionDetectionService;
+import android.security.intrusiondetection.IntrusionDetectionManager;
 import android.security.keystore.KeyStoreManager;
 import android.service.oemlock.IOemLockService;
 import android.service.oemlock.OemLockManager;
@@ -1025,6 +1027,25 @@
                     }
                 });
 
+        registerService(Context.AUTHENTICATION_POLICY_SERVICE,
+                AuthenticationPolicyManager.class,
+                new CachedServiceFetcher<AuthenticationPolicyManager>() {
+                    @Override
+                    public AuthenticationPolicyManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        if (!android.security.Flags.secureLockdown()) {
+                            throw new ServiceNotFoundException(
+                                    Context.AUTHENTICATION_POLICY_SERVICE);
+                        }
+
+                        final IBinder binder = ServiceManager.getServiceOrThrow(
+                                Context.AUTHENTICATION_POLICY_SERVICE);
+                        final IAuthenticationPolicyService service =
+                                IAuthenticationPolicyService.Stub.asInterface(binder);
+                        return new AuthenticationPolicyManager(ctx.getOuterContext(), service);
+                    }
+                });
+
         registerService(Context.TV_INTERACTIVE_APP_SERVICE, TvInteractiveAppManager.class,
                 new CachedServiceFetcher<TvInteractiveAppManager>() {
             @Override
@@ -1797,15 +1818,20 @@
                     }
                 });
 
-        registerService(Context.FORENSIC_SERVICE, ForensicManager.class,
-                new CachedServiceFetcher<ForensicManager>() {
+        registerService(Context.INTRUSION_DETECTION_SERVICE, IntrusionDetectionManager.class,
+                new CachedServiceFetcher<IntrusionDetectionManager>() {
                     @Override
-                    public ForensicManager createService(ContextImpl ctx)
+                    public IntrusionDetectionManager createService(ContextImpl ctx)
                             throws ServiceNotFoundException {
+                        if (!android.security.Flags.aflApi()) {
+                            throw new ServiceNotFoundException(
+                                    "Intrusion Detection is not supported");
+                        }
                         IBinder b = ServiceManager.getServiceOrThrow(
-                                Context.FORENSIC_SERVICE);
-                        IForensicService service = IForensicService.Stub.asInterface(b);
-                        return new ForensicManager(service);
+                                Context.INTRUSION_DETECTION_SERVICE);
+                        IIntrusionDetectionService service =
+                                IIntrusionDetectionService.Stub.asInterface(b);
+                        return new IntrusionDetectionManager(service);
                     }
                 });
 
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index aac963a..01cc9d8 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -340,10 +340,10 @@
     public int requestedVisibleTypes;
 
     /**
-     * Whether the top activity has requested to limit educational dialogs shown by the system.
+     * The timestamp of the top activity's last request to show the "Open in Browser" education.
      * @hide
      */
-    public boolean isTopActivityLimitSystemEducationDialogs;
+    public long topActivityRequestOpenInBrowserEducationTimestamp;
 
     /**
      * Encapsulate specific App Compat information.
@@ -493,8 +493,8 @@
                 && Objects.equals(capturedLink, that.capturedLink)
                 && capturedLinkTimestamp == that.capturedLinkTimestamp
                 && requestedVisibleTypes == that.requestedVisibleTypes
-                && isTopActivityLimitSystemEducationDialogs
-                    == that.isTopActivityLimitSystemEducationDialogs
+                && topActivityRequestOpenInBrowserEducationTimestamp
+                    == that.topActivityRequestOpenInBrowserEducationTimestamp
                 && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo)
                 && Objects.equals(topActivityMainWindowFrame, that.topActivityMainWindowFrame);
     }
@@ -571,7 +571,7 @@
         capturedLink = source.readTypedObject(Uri.CREATOR);
         capturedLinkTimestamp = source.readLong();
         requestedVisibleTypes = source.readInt();
-        isTopActivityLimitSystemEducationDialogs = source.readBoolean();
+        topActivityRequestOpenInBrowserEducationTimestamp = source.readLong();
         appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR);
         topActivityMainWindowFrame = source.readTypedObject(Rect.CREATOR);
     }
@@ -627,7 +627,7 @@
         dest.writeTypedObject(capturedLink, flags);
         dest.writeLong(capturedLinkTimestamp);
         dest.writeInt(requestedVisibleTypes);
-        dest.writeBoolean(isTopActivityLimitSystemEducationDialogs);
+        dest.writeLong(topActivityRequestOpenInBrowserEducationTimestamp);
         dest.writeTypedObject(appCompatTaskInfo, flags);
         dest.writeTypedObject(topActivityMainWindowFrame, flags);
     }
@@ -672,8 +672,8 @@
                 + " capturedLink=" + capturedLink
                 + " capturedLinkTimestamp=" + capturedLinkTimestamp
                 + " requestedVisibleTypes=" + requestedVisibleTypes
-                + " isTopActivityLimitSystemEducationDialogs="
-                + isTopActivityLimitSystemEducationDialogs
+                + " topActivityRequestOpenInBrowserEducationTimestamp="
+                + topActivityRequestOpenInBrowserEducationTimestamp
                 + " appCompatTaskInfo=" + appCompatTaskInfo
                 + " topActivityMainWindowFrame=" + topActivityMainWindowFrame
                 + "}";
diff --git a/core/java/android/app/jank/FrameOverrunHistogram.java b/core/java/android/app/jank/FrameOverrunHistogram.java
index e28ac12..3ad6531 100644
--- a/core/java/android/app/jank/FrameOverrunHistogram.java
+++ b/core/java/android/app/jank/FrameOverrunHistogram.java
@@ -39,7 +39,7 @@
      * Create a new instance of FrameOverrunHistogram.
      */
     public FrameOverrunHistogram() {
-        mBucketCounts = new int[sBucketEndpoints.length - 1];
+        mBucketCounts = new int[sBucketEndpoints.length];
     }
 
     /**
diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java
index 7525d04..7ceaeb3 100644
--- a/core/java/android/app/jank/JankDataProcessor.java
+++ b/core/java/android/app/jank/JankDataProcessor.java
@@ -59,7 +59,6 @@
      * @param appUid the uid of the app.
      */
     public void processJankData(List<JankData> jankData, String activityName, int appUid) {
-        mCurrentBatchCount++;
         // add all the previous and active states to the pending states list.
         mStateTracker.retrieveAllStates(mPendingStates);
 
@@ -79,9 +78,8 @@
             }
         }
         // At this point we have attributed all frames to a state.
-        if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) {
-            logMetricCounts();
-        }
+        incrementBatchCountAndMaybeLogStats();
+
         // return the StatData object back to the pool to be reused.
         jankDataProcessingComplete();
     }
@@ -91,7 +89,73 @@
      * stats
      */
     public void mergeJankStats(AppJankStats jankStats, String activityName) {
-        // TODO b/377572463 Add Merging Logic
+        // Each state has a key which is a combination of widget category, widget id and widget
+        // state, this key is also used to identify pending stats, a pending stat is essentially a
+        // state with frames associated with it.
+        String stateKey = mStateTracker.getStateKey(jankStats.getWidgetCategory(),
+                jankStats.getWidgetId(), jankStats.getWidgetState());
+
+        if (mPendingJankStats.containsKey(stateKey)) {
+            mergeExistingStat(stateKey, jankStats);
+        } else {
+            mergeNewStat(stateKey, activityName, jankStats);
+        }
+
+        incrementBatchCountAndMaybeLogStats();
+    }
+
+    private void mergeExistingStat(String stateKey, AppJankStats jankStat) {
+        PendingJankStat pendingStat = mPendingJankStats.get(stateKey);
+
+        pendingStat.mJankyFrames += jankStat.getJankyFrameCount();
+        pendingStat.mTotalFrames += jankStat.getTotalFrameCount();
+
+        mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets,
+                jankStat.getFrameOverrunHistogram().getBucketCounters());
+    }
+
+    private void mergeNewStat(String stateKey, String activityName, AppJankStats jankStats) {
+        // Check if we have space for a new stat
+        if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) {
+            return;
+        }
+
+        PendingJankStat pendingStat = mPendingJankStatsPool.acquire();
+        if (pendingStat == null) {
+            pendingStat = new PendingJankStat();
+
+        }
+        pendingStat.clearStats();
+
+        pendingStat.mActivityName = activityName;
+        pendingStat.mUid = jankStats.getUid();
+        pendingStat.mWidgetId = jankStats.getWidgetId();
+        pendingStat.mWidgetCategory = jankStats.getWidgetCategory();
+        pendingStat.mWidgetState = jankStats.getWidgetState();
+        pendingStat.mTotalFrames = jankStats.getTotalFrameCount();
+        pendingStat.mJankyFrames = jankStats.getJankyFrameCount();
+
+        mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets,
+                jankStats.getFrameOverrunHistogram().getBucketCounters());
+
+        mPendingJankStats.put(stateKey, pendingStat);
+    }
+
+    private void mergeOverrunHistograms(int[] mergeTarget, int[] mergeSource) {
+        // The length of each histogram should be identical, if they are not then its possible the
+        // buckets are not in sync, these records should not be recorded.
+        if (mergeTarget.length != mergeSource.length) return;
+
+        for (int i = 0; i < mergeTarget.length; i++) {
+            mergeTarget[i] += mergeSource[i];
+        }
+    }
+
+    private void incrementBatchCountAndMaybeLogStats() {
+        mCurrentBatchCount++;
+        if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) {
+            logMetricCounts();
+        }
     }
 
     /**
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
index 202281f..4695216 100644
--- a/core/java/android/app/jank/JankTracker.java
+++ b/core/java/android/app/jank/JankTracker.java
@@ -89,7 +89,12 @@
      * stats
      */
     public void mergeAppJankStats(AppJankStats appJankStats) {
-        mJankDataProcessor.mergeJankStats(appJankStats, mActivityName);
+        getHandler().post(new Runnable() {
+            @Override
+            public void run() {
+                mJankDataProcessor.mergeJankStats(appJankStats, mActivityName);
+            }
+        });
     }
 
     public void setActivityName(@NonNull String activityName) {
diff --git a/core/java/android/app/jank/StateTracker.java b/core/java/android/app/jank/StateTracker.java
index c86d5a5..21bb5e8 100644
--- a/core/java/android/app/jank/StateTracker.java
+++ b/core/java/android/app/jank/StateTracker.java
@@ -180,7 +180,11 @@
         }
     }
 
-    private String getStateKey(String widgetCategory, String widgetId, String widgetState) {
+    /**
+     * Returns a concatenated string of the inputs. This key can be used to retrieve both pending
+     * stats and the state that was used to create the pending stat.
+     */
+    public String getStateKey(String widgetCategory, String widgetId, String widgetState) {
         return widgetCategory + widgetId + widgetState;
     }
 
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6086f24..acad92c9 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -18,6 +18,7 @@
 
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
+import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
 
 import android.annotation.AttrRes;
 import android.annotation.CallbackExecutor;
@@ -4256,6 +4257,7 @@
             FINGERPRINT_SERVICE,
             //@hide: FACE_SERVICE,
             BIOMETRIC_SERVICE,
+            AUTHENTICATION_POLICY_SERVICE,
             MEDIA_ROUTER_SERVICE,
             TELEPHONY_SERVICE,
             TELEPHONY_SUBSCRIPTION_SERVICE,
@@ -4437,6 +4439,9 @@
      * web domain approval state.
      * <dt> {@link #DISPLAY_HASH_SERVICE} ("display_hash")
      * <dd> A {@link android.view.displayhash.DisplayHashManager} for management of display hashes.
+     * <dt> {@link #AUTHENTICATION_POLICY_SERVICE} ("authentication_policy")
+     * <dd> A {@link android.security.authenticationpolicy.AuthenticationPolicyManager}
+     * for managing authentication related policies on the device.
      * </dl>
      *
      * <p>Note:  System services obtained via this API may be closely associated with
@@ -4521,8 +4526,9 @@
      * @see android.content.pm.verify.domain.DomainVerificationManager
      * @see #DISPLAY_HASH_SERVICE
      * @see android.view.displayhash.DisplayHashManager
+     * @see #AUTHENTICATION_POLICY_SERVICE
+     * @see android.security.authenticationpolicy.AuthenticationPolicyManager
      */
-    // TODO(b/347269120): Re-add @Nullable
     public abstract Object getSystemService(@ServiceName @NonNull String name);
 
     /**
@@ -4543,7 +4549,8 @@
      * {@link android.os.BatteryManager}, {@link android.app.job.JobScheduler},
      * {@link android.app.usage.NetworkStatsManager},
      * {@link android.content.pm.verify.domain.DomainVerificationManager},
-     * {@link android.view.displayhash.DisplayHashManager}.
+     * {@link android.view.displayhash.DisplayHashManager}
+     * {@link android.security.authenticationpolicy.AuthenticationPolicyManager}.
      * </p>
      *
      * <p>
@@ -4568,7 +4575,6 @@
      */
     @SuppressWarnings("unchecked")
     @RavenwoodKeep
-    // TODO(b/347269120): Re-add @Nullable
     public final <T> T getSystemService(@NonNull Class<T> serviceClass) {
         // Because subclasses may override getSystemService(String) we cannot
         // perform a lookup by class alone.  We must first map the class to its
@@ -5183,6 +5189,18 @@
     public static final String AUTH_SERVICE = "auth";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve an {@link
+     * android.security.authenticationpolicy.AuthenticationPolicyManager}.
+     * @see #getSystemService
+     * @see android.security.authenticationpolicy.AuthenticationPolicyManager
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final String AUTHENTICATION_POLICY_SERVICE = "authentication_policy";
+
+    /**
      * Use with {@link #getSystemService(String)} to retrieve a
      * {@link android.hardware.fingerprint.FingerprintManager} for handling management
      * of fingerprints.
@@ -5695,12 +5713,12 @@
     public static final String BINARY_TRANSPARENCY_SERVICE = "transparency";
 
     /**
-     * System service name for ForensicService.
-     * The service manages the forensic info on device.
+     * System service name for IntrusionDetectionService.
+     * The service manages the intrusion detection info on device.
      * @hide
      */
     @FlaggedApi(android.security.Flags.FLAG_AFL_API)
-    public static final String FORENSIC_SERVICE = "forensic";
+    public static final String INTRUSION_DETECTION_SERVICE = "intrusion_detection";
 
     /**
      * System service name for the DeviceIdleManager.
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 23d17cb..413eb98 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -960,7 +960,6 @@
     }
 
     @Override
-    // TODO(b/347269120): Re-add @Nullable
     public Object getSystemService(String name) {
         return mBase.getSystemService(name);
     }
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index ce52825..b10f5e4 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1320,23 +1320,23 @@
             264301586L; // buganizer id
 
     /**
-     * Excludes the packages the override is applied to from the camera compatibility treatment
-     * in free-form windowing mode for fixed-orientation apps.
+     * Includes the packages the override is applied to in the camera compatibility treatment in
+     * free-form windowing mode for fixed-orientation apps.
      *
      * <p>In free-form windowing mode, the compatibility treatment emulates running on a portrait
      * device by letterboxing the app window and changing the camera characteristics to what apps
      * commonly expect in a portrait device: 90 and 270 degree sensor rotation for back and front
      * cameras, respectively, and setting display rotation to 0.
      *
-     * <p>Use this flag to disable the compatibility treatment for apps that do not respond well to
-     * the treatment.
+     * <p>Use this flag to enable the compatibility treatment for apps in which camera doesn't work
+     * well in freeform windowing.
      *
      * @hide
      */
     @ChangeId
     @Overridable
     @Disabled
-    public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT =
+    public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT =
             314961188L;
 
     /**
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 4285b0a..8243d88 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -20,6 +20,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.RequiresPermission;
+import android.health.connect.HealthPermissions;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Printer;
@@ -361,8 +362,10 @@
      * {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following
      * permissions:
      * {@link android.Manifest.permission#ACTIVITY_RECOGNITION},
-     * {@link android.Manifest.permission#BODY_SENSORS},
      * {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}.
+     * {@link android.health.connect.HealthPermissions#READ_HEART_RATE},
+     * {@link android.health.connect.HealthPermissions#READ_SKIN_TEMPERATURE},
+     * {@link android.health.connect.HealthPermissions#READ_OXYGEN_SATURATION},
      */
     @RequiresPermission(
             allOf = {
@@ -370,10 +373,13 @@
             },
             anyOf = {
                 Manifest.permission.ACTIVITY_RECOGNITION,
-                Manifest.permission.BODY_SENSORS,
                 Manifest.permission.HIGH_SAMPLING_RATE_SENSORS,
+                HealthPermissions.READ_HEART_RATE,
+                HealthPermissions.READ_SKIN_TEMPERATURE,
+                HealthPermissions.READ_OXYGEN_SATURATION,
             }
     )
+    @FlaggedApi(android.permission.flags.Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
     public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 1 << 8;
 
     /**
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index c83cf96..1d8209d 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -142,6 +142,11 @@
     private final boolean mIsSdkLibrary;
 
     /**
+     * Indicates if this apk is a static library.
+     */
+    private final boolean mIsStaticLibrary;
+
+    /**
      * List of SDK names used by this apk.
      */
     private final @NonNull List<String> mUsesSdkLibraries;
@@ -191,7 +196,7 @@
             Set<String> requiredSplitTypes, Set<String> splitTypes,
             boolean hasDeviceAdminReceiver, boolean isSdkLibrary,
             List<String> usesSdkLibraries, long[] usesSdkLibrariesVersionsMajor,
-            String[][] usesSdkLibrariesCertDigests,
+            String[][] usesSdkLibrariesCertDigests, boolean isStaticLibrary,
             List<String> usesStaticLibraries, long[] usesStaticLibrariesVersionsMajor,
             String[][] usesStaticLibrariesCertDigests,
             boolean updatableSystem,
@@ -229,6 +234,7 @@
         mRollbackDataPolicy = rollbackDataPolicy;
         mHasDeviceAdminReceiver = hasDeviceAdminReceiver;
         mIsSdkLibrary = isSdkLibrary;
+        mIsStaticLibrary = isStaticLibrary;
         mUsesSdkLibraries = usesSdkLibraries;
         mUsesSdkLibrariesVersionsMajor = usesSdkLibrariesVersionsMajor;
         mUsesSdkLibrariesCertDigests = usesSdkLibrariesCertDigests;
@@ -275,6 +281,7 @@
         mRollbackDataPolicy = 0;
         mHasDeviceAdminReceiver = false;
         mIsSdkLibrary = false;
+        mIsStaticLibrary = false;
         mUsesSdkLibraries = Collections.emptyList();
         mUsesSdkLibrariesVersionsMajor = null;
         mUsesSdkLibrariesCertDigests = null;
@@ -594,6 +601,14 @@
     }
 
     /**
+     * Indicates if this apk is a static library.
+     */
+    @DataClass.Generated.Member
+    public boolean isIsStaticLibrary() {
+        return mIsStaticLibrary;
+    }
+
+    /**
      * List of SDK names used by this apk.
      */
     @DataClass.Generated.Member
@@ -662,10 +677,10 @@
     }
 
     @DataClass.Generated(
-            time = 1730202160705L,
+            time = 1731589363302L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\nprivate final  boolean mHasDeviceAdminReceiver\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final  boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\nprivate final  boolean mHasDeviceAdminReceiver\nprivate final  boolean mIsSdkLibrary\nprivate final  boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final  boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 05c8f31..8ba556d 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -465,6 +465,7 @@
         boolean hasDeviceAdminReceiver = false;
 
         boolean isSdkLibrary = false;
+        boolean isStaticLibrary = false;
         List<String> usesSdkLibraries = new ArrayList<>();
         long[] usesSdkLibrariesVersionsMajor = new long[0];
         String[][] usesSdkLibrariesCertDigests = new String[0][0];
@@ -588,6 +589,9 @@
                                     /*allowDuplicates=*/ true);
                             break;
                         case TAG_USES_STATIC_LIBRARY:
+                            if (!android.content.pm.Flags.sdkDependencyInstaller()) {
+                                break;
+                            }
                             String usesStaticLibName = parser.getAttributeValue(
                                     ANDROID_RES_NAMESPACE, "name");
                             long usesStaticLibVersion = parser.getAttributeIntValue(
@@ -656,6 +660,7 @@
                                     SharedLibraryInfo.TYPE_SDK_PACKAGE));
                             break;
                         case TAG_STATIC_LIBRARY:
+                            isSdkLibrary = true;
                             // Mirrors ParsingPackageUtils#parseStaticLibrary until lite and full
                             // parsing are combined
                             String staticLibName = parser.getAttributeValue(
@@ -809,7 +814,7 @@
                         requiredSystemPropertyValue, minSdkVersion, targetSdkVersion,
                         rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second,
                         hasDeviceAdminReceiver, isSdkLibrary, usesSdkLibraries,
-                        usesSdkLibrariesVersionsMajor, usesSdkLibrariesCertDigests,
+                        usesSdkLibrariesVersionsMajor, usesSdkLibrariesCertDigests, isStaticLibrary,
                         usesStaticLibraries, usesStaticLibrariesVersions,
                         usesStaticLibrariesCertDigests, updatableSystem, emergencyInstaller,
                         declaredLibraries));
diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java
index 76b25fe..0e11eec 100644
--- a/core/java/android/content/pm/parsing/PackageLite.java
+++ b/core/java/android/content/pm/parsing/PackageLite.java
@@ -114,6 +114,10 @@
      * Indicates if this package is a sdk.
      */
     private final boolean mIsSdkLibrary;
+    /**
+     * Indicates if this package is a static library.
+     */
+    private final boolean mIsStaticLibrary;
 
     private final @NonNull List<String> mUsesSdkLibraries;
 
@@ -164,6 +168,7 @@
         mUsesSdkLibraries = baseApk.getUsesSdkLibraries();
         mUsesSdkLibrariesVersionsMajor = baseApk.getUsesSdkLibrariesVersionsMajor();
         mUsesSdkLibrariesCertDigests = baseApk.getUsesSdkLibrariesCertDigests();
+        mIsStaticLibrary = baseApk.isIsStaticLibrary();
         mUsesStaticLibraries = baseApk.getUsesStaticLibraries();
         mUsesStaticLibrariesVersions = baseApk.getUsesStaticLibrariesVersions();
         mUsesStaticLibrariesCertDigests = baseApk.getUsesStaticLibrariesCertDigests();
@@ -455,6 +460,14 @@
         return mIsSdkLibrary;
     }
 
+    /**
+     * Indicates if this package is a static library.
+     */
+    @DataClass.Generated.Member
+    public boolean isIsStaticLibrary() {
+        return mIsStaticLibrary;
+    }
+
     @DataClass.Generated.Member
     public @NonNull List<String> getUsesSdkLibraries() {
         return mUsesSdkLibraries;
@@ -499,10 +512,10 @@
     }
 
     @DataClass.Generated(
-            time = 1730203707341L,
+            time = 1731591578587L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mTargetSdk\nprivate final  int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final  int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mProfileableByShell\nprivate final  boolean mUseEmbeddedDex\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  java.util.List<java.lang.String> getAllApkPaths()\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mTargetSdk\nprivate final  int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final  int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mProfileableByShell\nprivate final  boolean mUseEmbeddedDex\nprivate final  boolean mIsSdkLibrary\nprivate final  boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  java.util.List<java.lang.String> getAllApkPaths()\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index f23c193..6fc7d90 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -105,3 +105,12 @@
     # This flag is used to control aapt2 behavior.
     is_fixed_read_only: true
 }
+
+flag {
+    name: "resources_minor_version_support"
+    is_exported: true
+    namespace: "resource_manager"
+    description: "Feature flag for supporting minor version in Resources"
+    bug: "373535266"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.java b/core/java/android/hardware/contexthub/HubEndpointInfo.java
index b1d5523..5265d56 100644
--- a/core/java/android/hardware/contexthub/HubEndpointInfo.java
+++ b/core/java/android/hardware/contexthub/HubEndpointInfo.java
@@ -156,7 +156,7 @@
         mRequiredPermissions = Arrays.asList(endpointInfo.requiredPermissions);
         mHubServiceInfos = new ArrayList<>(endpointInfo.services.length);
         for (int i = 0; i < endpointInfo.services.length; i++) {
-            mHubServiceInfos.set(i, new HubServiceInfo(endpointInfo.services[i]));
+            mHubServiceInfos.add(new HubServiceInfo(endpointInfo.services[i]));
         }
     }
 
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 3284761..ed510e4 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -281,4 +281,6 @@
     AidlInputGestureData[] getCustomInputGestures(int userId, int tag);
 
     AidlInputGestureData[] getAppLaunchBookmarks();
+
+    void resetLockedModifierState();
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index f824192..10224c1 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -260,7 +260,7 @@
     }
 
     /**
-     * Custom input gesture error: Input gesture already exists
+     * Custom input gesture result success
      *
      * @hide
      */
@@ -1590,6 +1590,21 @@
     }
 
     /**
+     * Resets locked modifier state (i.e.. Caps Lock, Num Lock, Scroll Lock state)
+     *
+     * @hide
+     */
+    @TestApi
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    public void resetLockedModifierState() {
+        try {
+            mIm.resetLockedModifierState();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * A callback used to be notified about battery state changes for an input device. The
      * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the
      * listener is successfully registered to provide the initial battery state of the device.
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 71b60cf..5a713cf 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -31,7 +31,7 @@
 import static com.android.hardware.input.Flags.touchpadThreeFingerTapShortcut;
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiKeyGestures;
 import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
 import static com.android.input.flags.Flags.keyboardRepeatKeys;
@@ -1155,6 +1155,6 @@
      * @hide
      */
     public static boolean doesKeyGestureEventHandlerSupportMultiKeyGestures() {
-        return useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures();
+        return useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiKeyGestures();
     }
 }
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index fee0749..0c89059 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -88,6 +88,17 @@
 }
 
 flag {
+    name: "input_manager_lifecycle_support"
+    namespace: "input"
+    description: "Add support for Lifecycle support in input manager"
+    bug: "362473586"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     namespace: "input_native"
     name: "manage_key_gestures"
     description: "Manage key gestures through Input APIs"
@@ -103,8 +114,8 @@
 }
 
 flag {
-    namespace: "input_native"
-    name: "use_key_gesture_event_handler_multi_press_gestures"
+    namespace: "input"
+    name: "use_key_gesture_event_handler_multi_key_gestures"
     description: "Use KeyGestureEvent handler APIs to control multi key press gestures"
     bug: "358569822"
 }
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index dadb5c38..977c5bd 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -407,6 +407,12 @@
     private boolean mUsingCtrlShiftShortcut = false;
 
     /**
+     * Last handwriting bounds used for stylus handwriting
+     * {@link #setStylusHandwritingRegion(Region)}.
+     */
+    private Region mLastHandwritingRegion;
+
+    /**
      * Returns whether {@link InputMethodService} is responsible for rendering the back button and
      * the IME switcher button or not when the gestural navigation is enabled.
      *
@@ -1532,6 +1538,7 @@
                     return;
                 }
                 editorInfo.makeCompatible(getApplicationInfo().targetSdkVersion);
+                mLastHandwritingRegion = null;
                 getInputMethodInternal().restartInput(new RemoteInputConnection(ric, sessionId),
                         editorInfo);
             }
@@ -2840,6 +2847,7 @@
             mHandler.removeCallbacks(mFinishHwRunnable);
         }
         mFinishHwRunnable = null;
+        mLastHandwritingRegion = null;
 
         final int requestId = mHandwritingRequestId.getAsInt();
         mHandwritingRequestId = OptionalInt.empty();
@@ -3166,6 +3174,40 @@
         registerDefaultOnBackInvokedCallback();
     }
 
+    /**
+     * Sets a new stylus handwriting region as user continues to write on an editor on screen.
+     * Stylus strokes that are started within the {@code touchableRegion} are treated as
+     * continuation of handwriting and all the events outside are passed-through to the IME target
+     * app, causing stylus handwriting to finish {@link #finishStylusHandwriting()}.
+     * By default, {@link WindowManager#getMaximumWindowMetrics()} is handwritable and
+     * {@code touchableRegion} resets after each handwriting session.
+     * <p>
+     * For example, the IME can use this API to dynamically expand the stylus handwriting region on
+     * every stylus stroke as user continues to write on an editor. The region should grow around
+     * the last stroke so that a UI element below the IME window is still interactable when it is
+     * spaced sufficiently away (~2 character dimensions) from last stroke.
+     * </p>
+     * <p>
+     * Note: Setting handwriting touchable region is supported on IMEs that support stylus
+     * handwriting {@link InputMethodInfo#supportsStylusHandwriting()}.
+     * </p>
+     *
+     * @param handwritingRegion new stylus handwritable {@link Region} that can accept stylus touch.
+     */
+    @FlaggedApi(Flags.FLAG_ADAPTIVE_HANDWRITING_BOUNDS)
+    public final void setStylusHandwritingRegion(@NonNull Region handwritingRegion) {
+        if (handwritingRegion.equals(mLastHandwritingRegion)) {
+            Log.v(TAG, "Failed to set setStylusHandwritingRegion():"
+                    + " same region set twice.");
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Setting new handwriting region for stylus handwriting "
+                    + handwritingRegion + " from last " + mLastHandwritingRegion);
+        }
+        mLastHandwritingRegion = handwritingRegion;
+    }
 
     /**
      * Registers an {@link OnBackInvokedCallback} to handle back invocation when ahead-of-time
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 036ccd8..4c9f08d 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -123,11 +123,6 @@
         // We can lift this restriction in the future after we've made it possible for test authors
         // to test Looper and MessageQueue without resorting to reflection.
 
-        // Holdback study.
-        if (mUseConcurrent && Flags.messageQueueForceLegacy()) {
-            mUseConcurrent = false;
-        }
-
         mQuitAllowed = quitAllowed;
         mPtr = nativeInit();
     }
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index 224b10d..2b0042d 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 
 import com.android.internal.util.Preconditions;
@@ -106,7 +107,9 @@
      * All timings should be in {@link SystemClock#uptimeNanos()}.
      */
     public static class Session implements Closeable {
-        private long mNativeSessionPtr;
+        /** @hide */
+        @UnsupportedAppUsage
+        public long mNativeSessionPtr;
 
         /** @hide */
         public Session(long nativeSessionPtr) {
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 07fded1..9e7bf47 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1823,14 +1823,16 @@
     }
 
     /**
-     * Returns the interactive state for a specific display, which may not be the same as the
-     * global wakefulness (which is true when any display is awake).
+     * Returns true if the specified display is in an interactive state. This may not be the
+     * same as the global wakefulness (which is true when any display is interactive).
+     * @see #isInteractive()
      *
-     * @param displayId
-     * @return whether the given display is present and interactive, or false
+     * @param displayId The Display ID to check for interactivity.
+     * @return True if the display is in an interactive state.
      *
      * @hide
      */
+    @TestApi
     public boolean isInteractive(int displayId) {
         return mInteractiveCache.query(displayId);
     }
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index f6bc389..0a0e806 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -18,6 +18,7 @@
 
 import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
@@ -34,6 +35,7 @@
 import android.hardware.vibrator.V1_0.EffectStrength;
 import android.hardware.vibrator.V1_3.Effect;
 import android.net.Uri;
+import android.os.vibrator.BasicPwleSegment;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
@@ -1713,10 +1715,13 @@
         /**
          * Add a haptic primitive to the end of the current composition.
          *
+         * <p>Similar to {@link #addPrimitive(int, float, int, int)}, but default
+         * delay type applied is {@link #DELAY_TYPE_PAUSE}.
+         *
          * @param primitiveId The primitive to add
          * @param scale The scale to apply to the intensity of the primitive.
-         * @param delay The amount of time in milliseconds to wait before playing this primitive,
-         *              starting at the time the previous element in this composition is finished.
+         * @param delay The amount of time in milliseconds to wait between the end of the last
+         *              primitive and the beginning of this one (i.e. a pause in the composition).
          * @return This {@link Composition} object to enable adding multiple elements in one chain.
          */
         @NonNull
@@ -1843,6 +1848,8 @@
      *     .build();
      * }</pre>
      *
+     * <p>The builder automatically starts all effects at 0 amplitude.
+     *
      * <p>It is crucial to ensure that the frequency range used in your effect is compatible with
      * the device's capabilities. The framework will not play any frequencies that fall partially
      * or completely outside the device's supported range. It will also not attempt to correct or
@@ -1908,7 +1915,7 @@
                         firstSegment.getEndAmplitude(),
                         initialFrequencyHz, // Update start frequency
                         firstSegment.getEndFrequencyHz(),
-                        (int) firstSegment.getDuration()));
+                        firstSegment.getDuration()));
             }
 
             return this;
@@ -1930,25 +1937,26 @@
          * transition as quickly as possible, use
          * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
          *
-         * @param amplitude   The amplitude value between 0 and 1, inclusive. 0 represents the
-         *                    vibrator being off, and 1 represents the maximum achievable amplitude
-         *                    at this frequency.
-         * @param frequencyHz The frequency in Hz, must be greater than zero.
-         * @param timeMillis  The transition time in milliseconds.
+         * @param amplitude      The amplitude value between 0 and 1, inclusive. 0 represents the
+         *                       vibrator being off, and 1 represents the maximum achievable
+         *                       amplitude
+         *                       at this frequency.
+         * @param frequencyHz    The frequency in Hz, must be greater than zero.
+         * @param durationMillis The transition time in milliseconds.
          */
         @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
         @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
         @NonNull
         public WaveformEnvelopeBuilder addControlPoint(
                 @FloatRange(from = 0, to = 1) float amplitude,
-                @FloatRange(from = 0) float frequencyHz, int timeMillis) {
+                @FloatRange(from = 0) float frequencyHz, @DurationMillisLong long durationMillis) {
 
             if (Float.isNaN(mLastFrequencyHz)) {
                 mLastFrequencyHz = frequencyHz;
             }
 
             mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz,
-                    timeMillis));
+                    durationMillis));
 
             mLastAmplitude = amplitude;
             mLastFrequencyHz = frequencyHz;
@@ -1980,6 +1988,168 @@
     }
 
     /**
+     * A builder for waveform effects defined by their envelope, designed to provide a consistent
+     * haptic perception across devices with varying capabilities.
+     *
+     * <p>This builder simplifies the creation of waveform effects by automatically adapting them
+     * to different devices based on their capabilities. Effects are defined by control points
+     * specifying target vibration intensity and sharpness, along with durations to reach those
+     * targets. The vibrator will smoothly transition between these control points.
+     *
+     * <p><b>Intensity:</b> Defines the overall strength of the vibration, ranging from
+     * 0 (off) to 1 (maximum achievable strength). Higher values result in stronger
+     * vibrations. Supported intensity values guarantee sensitivity levels (SL) above
+     * 10 dB SL to ensure human perception.
+     *
+     * <p><b>Sharpness:</b> Defines the crispness of the vibration, ranging from 0 to 1.
+     * Lower values produce smoother vibrations, while higher values create a sharper,
+     * more snappy sensation. Sharpness is mapped to its equivalent frequency within
+     * the device's supported frequency range.
+     *
+     * <p>While this builder handles most of the adaptation logic, it does come with some
+     * limitations:
+     * <ul>
+     *     <li>It may not use the full range of frequencies</li>
+     *     <li>It's restricted to a frequency range that can generate output of at least 10 db
+     *     SL</li>
+     *     <li>Effects must end with a zero intensity control point. Failure to end at a zero
+     *     intensity control point will result in an {@link IllegalStateException}.</li>
+     * </ul>
+     *
+     * <p>The builder automatically starts all effects at 0 intensity.
+     *
+     * <p>To avoid these limitations and to have more control over the effects output, use
+     * {@link WaveformEnvelopeBuilder}, where direct amplitude and frequency values can be used.
+     *
+     * <p>For optimal cross-device consistency, it's recommended to limit the number of control
+     * points to a maximum of 16. However this is not mandatory, and if a pattern exceeds the
+     * maximum number of allowed control points, the framework will automatically break down the
+     * effect to ensure it plays correctly.
+     *
+     * <p>For example, the following code creates a vibration effect that ramps up the intensity
+     * from a low-pitched to a high-pitched strong vibration over 500ms and then ramps it down to
+     * 0 (off) over 100ms:
+     *
+     * <pre>{@code
+     * VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+     *     .setInitialSharpness(0.0f)
+     *     .addControlPoint(1.0f, 1.0f, 500)
+     *     .addControlPoint(0.0f, 1.0f, 100)
+     *     .build();
+     * }</pre>
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public static final class BasicEnvelopeBuilder {
+
+        private ArrayList<BasicPwleSegment> mSegments = new ArrayList<>();
+        private float mLastIntensity = 0f;
+        private float mLastSharpness = Float.NaN;
+
+        public BasicEnvelopeBuilder() {}
+
+        /**
+         * Sets the initial sharpness for the basic envelope effect.
+         *
+         * <p>The effect will start vibrating at this sharpness when it transitions to the
+         * intensity and sharpness defined by the first control point.
+         *
+         * <p> The sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
+         * values translate to smoother vibrations, while higher values create a sharper more snappy
+         * sensation. This value is mapped to the supported frequency range of the device.
+         *
+         * @param initialSharpness The starting sharpness of the vibration in the range of [0, 1].
+         */
+        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+        @SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial sharpness once set.
+        @NonNull
+        public BasicEnvelopeBuilder setInitialSharpness(
+                @FloatRange(from = 0, to = 1) float initialSharpness) {
+
+            if (mSegments.isEmpty()) {
+                mLastSharpness = initialSharpness;
+            } else {
+                BasicPwleSegment firstSegment = mSegments.getFirst();
+                mSegments.set(0, new BasicPwleSegment(
+                        firstSegment.getStartIntensity(),
+                        firstSegment.getEndIntensity(),
+                        initialSharpness, // Update start sharpness
+                        firstSegment.getEndSharpness(),
+                        firstSegment.getDuration()));
+            }
+
+            return this;
+        }
+
+        /**
+         * Adds a new control point to the end of this waveform envelope.
+         *
+         * <p>Intensity defines the overall strength of the vibration, ranging from 0 (off) to 1
+         * (maximum achievable strength). Higher values translate to stronger vibrations.
+         *
+         * <p>Sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
+         * values translate to smoother vibrations, while higher values create a sharper more snappy
+         * sensation. This value is mapped to the supported frequency range of the device.
+         *
+         * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
+         * from the previous control point to this new one. It must be greater than zero. To
+         * transition as quickly as possible, use
+         * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
+         *
+         * @param intensity      The target vibration intensity, ranging from 0 (off) to 1 (maximum
+         *                       strength).
+         * @param sharpness      The target sharpness, ranging from 0 (smoothest) to 1 (sharpest).
+         * @param durationMillis The transition time in milliseconds.
+         */
+        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+        @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
+        @NonNull
+        public BasicEnvelopeBuilder addControlPoint(
+                @FloatRange(from = 0, to = 1) float intensity,
+                @FloatRange(from = 0, to = 1) float sharpness,
+                @DurationMillisLong long durationMillis) {
+
+            if (Float.isNaN(mLastSharpness)) {
+                mLastSharpness = sharpness;
+            }
+
+            mSegments.add(new BasicPwleSegment(mLastIntensity, intensity, mLastSharpness, sharpness,
+                    durationMillis));
+
+            mLastIntensity = intensity;
+            mLastSharpness = sharpness;
+
+            return this;
+        }
+
+        /**
+         * Build the waveform as a single {@link VibrationEffect}.
+         *
+         * <p>The {@link BasicEnvelopeBuilder} object is still valid after this call, so you can
+         * continue adding more primitives to it and generating more {@link VibrationEffect}s by
+         * calling this method again.
+         *
+         * @return The {@link VibrationEffect} resulting from the list of control points.
+         * @throws IllegalStateException if the last control point does not end at zero intensity.
+         */
+        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+        @NonNull
+        public VibrationEffect build() {
+            if (mSegments.isEmpty()) {
+                throw new IllegalStateException(
+                        "BasicEnvelopeBuilder must have at least one control point to build.");
+            }
+            if (mSegments.getLast().getEndIntensity() != 0) {
+                throw new IllegalStateException(
+                        "Basic envelope effects must end at a zero intensity control point.");
+            }
+            VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
+            effect.validate();
+            return effect;
+        }
+
+    }
+
+    /**
      * A builder for waveform haptic effects.
      *
      * <p>Waveform vibrations constitute of one or more timed transitions to new sets of vibration
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 9b1bf05..2ef8764 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -4,15 +4,6 @@
 
 # keep-sorted start block=yes newline_separated=yes
 flag {
-     # Holdback study for concurrent MessageQueue.
-     # Do not promote beyond trunkfood.
-     namespace: "system_performance"
-     name: "message_queue_force_legacy"
-     description: "Whether to holdback concurrent MessageQueue (force legacy)."
-     bug: "336880969"
-}
-
-flag {
     name: "adpf_gpu_report_actual_work_duration"
     is_exported: true
     namespace: "game"
diff --git a/core/java/android/os/vibrator/BasicPwleSegment.java b/core/java/android/os/vibrator/BasicPwleSegment.java
new file mode 100644
index 0000000..ed68173
--- /dev/null
+++ b/core/java/android/os/vibrator/BasicPwleSegment.java
@@ -0,0 +1,222 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * A {@link VibrationEffectSegment} that represents a smooth transition from the starting
+ * intensity and sharpness to new values over a specified duration.
+ *
+ * <p>The intensity and sharpness are expressed by float values in the range [0, 1], where
+ * intensity represents the user-perceived strength of the vibration, while sharpness represents
+ * the crispness of the vibration.
+ *
+ * @hide
+ */
+@TestApi
+@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+public final class BasicPwleSegment extends VibrationEffectSegment {
+    private final float mStartIntensity;
+    private final float mEndIntensity;
+    private final float mStartSharpness;
+    private final float mEndSharpness;
+    private final long mDuration;
+
+    BasicPwleSegment(@NonNull Parcel in) {
+        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readLong());
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public BasicPwleSegment(float startIntensity, float endIntensity, float startSharpness,
+            float endSharpness, long duration) {
+        mStartIntensity = startIntensity;
+        mEndIntensity = endIntensity;
+        mStartSharpness = startSharpness;
+        mEndSharpness = endSharpness;
+        mDuration = duration;
+    }
+
+    public float getStartIntensity() {
+        return mStartIntensity;
+    }
+
+    public float getEndIntensity() {
+        return mEndIntensity;
+    }
+
+    public float getStartSharpness() {
+        return mStartSharpness;
+    }
+
+    public float getEndSharpness() {
+        return mEndSharpness;
+    }
+
+    @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof BasicPwleSegment)) {
+            return false;
+        }
+        BasicPwleSegment other = (BasicPwleSegment) o;
+        return Float.compare(mStartIntensity, other.mStartIntensity) == 0
+                && Float.compare(mEndIntensity, other.mEndIntensity) == 0
+                && Float.compare(mStartSharpness, other.mStartSharpness) == 0
+                && Float.compare(mEndSharpness, other.mEndSharpness) == 0
+                && mDuration == other.mDuration;
+    }
+
+    /** @hide */
+    @Override
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+        return vibratorInfo.areEnvelopeEffectsSupported();
+    }
+
+    /** @hide */
+    @Override
+    public boolean isHapticFeedbackCandidate() {
+        return true;
+    }
+
+    /** @hide */
+    @Override
+    public void validate() {
+        Preconditions.checkArgumentInRange(mStartSharpness, 0f, 1f, "startSharpness");
+        Preconditions.checkArgumentInRange(mEndSharpness, 0f, 1f, "endSharpness");
+        Preconditions.checkArgumentInRange(mStartIntensity, 0f, 1f, "startIntensity");
+        Preconditions.checkArgumentInRange(mEndIntensity, 0f, 1f, "endIntensity");
+        Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero.");
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public BasicPwleSegment resolve(int defaultAmplitude) {
+        return this;
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public BasicPwleSegment scale(float scaleFactor) {
+        float newStartIntensity = VibrationEffect.scale(mStartIntensity, scaleFactor);
+        float newEndIntensity = VibrationEffect.scale(mEndIntensity, scaleFactor);
+        if (Float.compare(mStartIntensity, newStartIntensity) == 0
+                && Float.compare(mEndIntensity, newEndIntensity) == 0) {
+            return this;
+        }
+        return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness,
+                mEndSharpness,
+                mDuration);
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public BasicPwleSegment scaleLinearly(float scaleFactor) {
+        float newStartIntensity = VibrationEffect.scaleLinearly(mStartIntensity, scaleFactor);
+        float newEndIntensity = VibrationEffect.scaleLinearly(mEndIntensity, scaleFactor);
+        if (Float.compare(mStartIntensity, newStartIntensity) == 0
+                && Float.compare(mEndIntensity, newEndIntensity) == 0) {
+            return this;
+        }
+        return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness,
+                mEndSharpness,
+                mDuration);
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public BasicPwleSegment applyEffectStrength(int effectStrength) {
+        return this;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mStartIntensity, mEndIntensity, mStartSharpness, mEndSharpness,
+                mDuration);
+    }
+
+    @Override
+    public String toString() {
+        return "BasicPwle{startIntensity=" + mStartIntensity
+                + ", endIntensity=" + mEndIntensity
+                + ", startSharpness=" + mStartSharpness
+                + ", endSharpness=" + mEndSharpness
+                + ", duration=" + mDuration
+                + "}";
+    }
+
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format(Locale.US, "Pwle=%dms(intensity=%.2f @ %.2f to %.2f @ %.2f)",
+                mDuration,
+                mStartIntensity,
+                mStartSharpness,
+                mEndIntensity,
+                mEndSharpness);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(PARCEL_TOKEN_PWLE);
+        dest.writeFloat(mStartIntensity);
+        dest.writeFloat(mEndIntensity);
+        dest.writeFloat(mStartSharpness);
+        dest.writeFloat(mEndSharpness);
+        dest.writeLong(mDuration);
+    }
+
+    @NonNull
+    public static final Creator<BasicPwleSegment> CREATOR =
+            new Creator<BasicPwleSegment>() {
+                @Override
+                public BasicPwleSegment createFromParcel(Parcel in) {
+                    // Skip the type token
+                    in.readInt();
+                    return new BasicPwleSegment(in);
+                }
+
+                @Override
+                public BasicPwleSegment[] newArray(int size) {
+                    return new BasicPwleSegment[size];
+                }
+            };
+}
diff --git a/core/java/android/os/vibrator/PwlePoint.java b/core/java/android/os/vibrator/PwlePoint.java
index ea3ae6c..4be5c68 100644
--- a/core/java/android/os/vibrator/PwlePoint.java
+++ b/core/java/android/os/vibrator/PwlePoint.java
@@ -63,4 +63,12 @@
     public int hashCode() {
         return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis);
     }
+
+    @Override
+    public String toString() {
+        return "PwlePoint{amplitude=" + mAmplitude
+                + ", frequency=" + mFrequencyHz
+                + ", time=" + mTimeMillis
+                + "}";
+    }
 }
diff --git a/core/java/android/os/vibrator/PwleSegment.java b/core/java/android/os/vibrator/PwleSegment.java
index 9074bde..942b7b3 100644
--- a/core/java/android/os/vibrator/PwleSegment.java
+++ b/core/java/android/os/vibrator/PwleSegment.java
@@ -44,16 +44,16 @@
     private final float mStartFrequencyHz;
     private final float mEndAmplitude;
     private final float mEndFrequencyHz;
-    private final int mDuration;
+    private final long mDuration;
 
     PwleSegment(@android.annotation.NonNull Parcel in) {
-        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt());
+        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readLong());
     }
 
     /** @hide */
     @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz,
-            float endFrequencyHz, int duration) {
+            float endFrequencyHz, long duration) {
         mStartAmplitude = startAmplitude;
         mEndAmplitude = endAmplitude;
         mStartFrequencyHz = startFrequencyHz;
@@ -213,7 +213,7 @@
         dest.writeFloat(mEndAmplitude);
         dest.writeFloat(mStartFrequencyHz);
         dest.writeFloat(mEndFrequencyHz);
-        dest.writeInt(mDuration);
+        dest.writeLong(mDuration);
     }
 
     @android.annotation.NonNull
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index b934e11..325a058 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -47,6 +47,7 @@
     static final int PARCEL_TOKEN_STEP = 3;
     static final int PARCEL_TOKEN_RAMP = 4;
     static final int PARCEL_TOKEN_PWLE = 5;
+    static final int PARCEL_TOKEN_BASIC_PWLE = 6;
 
     /** Prevent subclassing from outside of this package */
     VibrationEffectSegment() {
@@ -228,7 +229,12 @@
                             if (Flags.normalizedPwleEffects()) {
                                 return new PwleSegment(in);
                             }
-                            // Fall through if the flag is not enabled.
+                            // Fall through to default if the flag is not enabled.
+                        case PARCEL_TOKEN_BASIC_PWLE:
+                            if (Flags.normalizedPwleEffects()) {
+                                return new BasicPwleSegment(in);
+                            }
+                            // Fall through to default if the flag is not enabled.
                         default:
                             throw new IllegalStateException(
                                     "Unexpected vibration event type token in parcel.");
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index b5a822b..55ba4af 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -418,3 +418,21 @@
     description: "Add new checkOp APIs that accept attributionTag"
     bug: "240617242"
 }
+
+flag {
+    name: "device_policy_management_role_split_create_managed_profile_enabled"
+    is_fixed_read_only: true
+    is_exported: true
+    namespace: "enterprise"
+    description: "Gives the device policy management role the ability to create a managed profile using new APIs"
+    bug: "375382324"
+}
+
+flag {
+    name: "use_profile_labels_for_default_app_section_titles"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "profile_experiences"
+    description: "Use profile labels from UserManager for default app section titles to allow partner customization"
+    bug: "358369931"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a35c9c1..19b0c6f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -20454,6 +20454,29 @@
              * @hide
              */
             public static final String AUTO_BEDTIME_MODE = "auto_bedtime_mode";
+
+            /**
+             * Indicates that all elements of the system status tray on wear should be rendered
+             * by default wear system.
+             *
+             * @hide
+             */
+            public static final int STATUS_TRAY_CONFIGURATION_DEFAULT = 0;
+
+            /**
+             * Indicates that all elements of the system status tray on wear should be hidden.
+             *
+             * @hide
+             */
+            public static final int STATUS_TRAY_CONFIGURATION_SYSTEM_HIDDEN = 1;
+
+            /**
+             * Configuration of system status tray in wear.
+             *
+             * @hide
+             */
+            public static final String WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION =
+                    "wear_system_status_tray_configuration";
         }
     }
 
diff --git a/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java b/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java
new file mode 100644
index 0000000..75abd5f
--- /dev/null
+++ b/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java
@@ -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 android.security.authenticationpolicy;
+
+import static android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE;
+import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * AuthenticationPolicyManager is a centralized interface for managing authentication related
+ * policies on the device. This includes device locking capabilities to protect users in "at risk"
+ * environments.
+ *
+ * AuthenticationPolicyManager is designed to protect Android users by integrating with apps and
+ * key system components, such as the lock screen. It is not related to enterprise control surfaces
+ * and does not offer additional administrative controls.
+ *
+ * <p>
+ * To use this class, call {@link #enableSecureLockDevice} to enable secure lock on the device.
+ * This will require the caller to have the
+ * {@link android.Manifest.permission#MANAGE_SECURE_LOCK_DEVICE} permission.
+ *
+ * <p>
+ * To disable secure lock on the device, call {@link #disableSecureLockDevice}. This will require
+ * the caller to have the {@link android.Manifest.permission#MANAGE_SECURE_LOCK_DEVICE} permission.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_SECURE_LOCKDOWN)
+@SystemService(Context.AUTHENTICATION_POLICY_SERVICE)
+public final class AuthenticationPolicyManager {
+    private static final String TAG = "AuthenticationPolicyManager";
+
+    @NonNull private final IAuthenticationPolicyService mAuthenticationPolicyService;
+    @NonNull private final Context mContext;
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice} and {@link
+     * #disableSecureLockDevice}.
+     *
+     * Secure lock device request status unknown.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_UNKNOWN = 0;
+
+    /**
+     * Success result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}.
+     *
+     * Secure lock device request successful.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int SUCCESS = 1;
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}.
+     *
+     * Secure lock device is unsupported.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_UNSUPPORTED = 2;
+
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}.
+     *
+     * Invalid secure lock device request params provided.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_INVALID_PARAMS = 3;
+
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}.
+     *
+     * Secure lock device is unavailable because there are no biometrics enrolled on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_NO_BIOMETRICS_ENROLLED = 4;
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}.
+     *
+     * Secure lock device is unavailable because the device has no biometric hardware or the
+     * biometric sensors do not meet
+     * {@link android.hardware.biometrics.BiometricManager.Authenticators#BIOMETRIC_STRONG}
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_INSUFFICIENT_BIOMETRICS = 5;
+
+    /**
+     * Error result code for {@link #enableSecureLockDevice}.
+     *
+     * Secure lock is already enabled.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public static final int ERROR_ALREADY_ENABLED = 6;
+
+    /**
+     * Communicates the current status of a request to enable secure lock on the device.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"ENABLE_SECURE_LOCK_DEVICE_STATUS_"}, value = {
+            ERROR_UNKNOWN,
+            SUCCESS,
+            ERROR_UNSUPPORTED,
+            ERROR_INVALID_PARAMS,
+            ERROR_NO_BIOMETRICS_ENROLLED,
+            ERROR_INSUFFICIENT_BIOMETRICS,
+            ERROR_ALREADY_ENABLED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EnableSecureLockDeviceRequestStatus {}
+
+    /**
+     * Communicates the current status of a request to disable secure lock on the device.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"DISABLE_SECURE_LOCK_DEVICE_STATUS_"}, value = {
+            ERROR_UNKNOWN,
+            SUCCESS,
+            ERROR_UNSUPPORTED,
+            ERROR_INVALID_PARAMS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DisableSecureLockDeviceRequestStatus {}
+
+    /** @hide */
+    public AuthenticationPolicyManager(@NonNull Context context,
+            @NonNull IAuthenticationPolicyService authenticationPolicyService) {
+        mContext = context;
+        mAuthenticationPolicyService = authenticationPolicyService;
+    }
+
+    /**
+     * Called by a privileged component to remotely enable secure lock on the device.
+     *
+     * Secure lock is an enhanced security state that restricts access to sensitive data (app
+     * notifications, widgets, quick settings, assistant, etc) and requires multi-factor
+     * authentication for device entry, such as
+     * {@link android.hardware.biometrics.BiometricManager.Authenticators#DEVICE_CREDENTIAL} and
+     * {@link android.hardware.biometrics.BiometricManager.Authenticators#BIOMETRIC_STRONG}.
+     *
+     * If secure lock is already enabled when this method is called, it will return
+     * {@link ERROR_ALREADY_ENABLED}.
+     *
+     * @param params EnableSecureLockDeviceParams for caller to supply params related to the secure
+     *               lock device request
+     * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the secure lock
+     * device request
+     *
+     * @hide
+     */
+    @EnableSecureLockDeviceRequestStatus
+    @RequiresPermission(MANAGE_SECURE_LOCK_DEVICE)
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public int enableSecureLockDevice(@NonNull EnableSecureLockDeviceParams params) {
+        try {
+            return mAuthenticationPolicyService.enableSecureLockDevice(params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Called by a privileged component to disable secure lock on the device.
+     *
+     * If secure lock is already disabled when this method is called, it will return
+     * {@link SUCCESS}.
+     *
+     * @param params @DisableSecureLockDeviceParams for caller to supply params related to the
+     *               secure lock device request
+     * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the secure lock
+     * device request
+     *
+     * @hide
+     */
+    @DisableSecureLockDeviceRequestStatus
+    @RequiresPermission(MANAGE_SECURE_LOCK_DEVICE)
+    @SystemApi
+    @FlaggedApi(FLAG_SECURE_LOCKDOWN)
+    public int disableSecureLockDevice(@NonNull DisableSecureLockDeviceParams params) {
+        try {
+            return mAuthenticationPolicyService.disableSecureLockDevice(params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl
similarity index 84%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl
index a321fb0..81f7726 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl
@@ -13,8 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.security.authenticationpolicy;
 
-package android.security.forensic;
-
-/** {@hide} */
-parcelable ForensicEvent;
+/**
+ * @hide
+ */
+parcelable DisableSecureLockDeviceParams;
\ No newline at end of file
diff --git a/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java
new file mode 100644
index 0000000..64a3f0f
--- /dev/null
+++ b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java
@@ -0,0 +1,82 @@
+/*
+ * 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.security.authenticationpolicy;
+
+import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Parameters related to a request to disable secure lock on the device.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_SECURE_LOCKDOWN)
+public final class DisableSecureLockDeviceParams implements Parcelable {
+
+    /**
+     * Client message associated with the request to disable secure lock on the device. This message
+     * will be shown on the device when secure lock mode is disabled.
+     */
+    private final @NonNull String mMessage;
+
+    /**
+     * Creates DisableSecureLockDeviceParams with the given params.
+     *
+     * @param message Allows clients to pass in a message with information about the request to
+     *                disable secure lock on the device. This message will be shown to the user when
+     *                secure lock mode is disabled. If an empty string is provided, it will default
+     *                to a system-defined string (e.g. "Secure lock mode has been disabled.")
+     */
+    public DisableSecureLockDeviceParams(@NonNull String message) {
+        mMessage = message;
+    }
+
+    private DisableSecureLockDeviceParams(@NonNull Parcel in) {
+        mMessage = Objects.requireNonNull(in.readString8());
+    }
+
+    public static final @NonNull Creator<DisableSecureLockDeviceParams> CREATOR =
+            new Creator<DisableSecureLockDeviceParams>() {
+                @Override
+                public DisableSecureLockDeviceParams createFromParcel(Parcel in) {
+                    return new DisableSecureLockDeviceParams(in);
+                }
+
+                @Override
+                public DisableSecureLockDeviceParams[] newArray(int size) {
+                    return new DisableSecureLockDeviceParams[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mMessage);
+    }
+}
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl
similarity index 85%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl
index a321fb0..9e496f8 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl
@@ -13,8 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.security.authenticationpolicy;
 
-package android.security.forensic;
-
-/** {@hide} */
-parcelable ForensicEvent;
+/**
+ * @hide
+ */
+parcelable EnableSecureLockDeviceParams;
\ No newline at end of file
diff --git a/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java
new file mode 100644
index 0000000..1d72772
--- /dev/null
+++ b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java
@@ -0,0 +1,82 @@
+/*
+ * 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.security.authenticationpolicy;
+
+import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+
+/**
+ * Parameters related to a request to enable secure lock on the device.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_SECURE_LOCKDOWN)
+public final class EnableSecureLockDeviceParams implements Parcelable {
+
+    /**
+     * Client message associated with the request to enable secure lock on the device. This message
+     * will be shown on the device when secure lock mode is enabled.
+     */
+    private final @NonNull String mMessage;
+
+    /**
+     * Creates EnableSecureLockDeviceParams with the given params.
+     *
+     * @param message Allows clients to pass in a message with information about the request to
+     *                enable secure lock on the device. This message will be shown to the user when
+     *                secure lock mode is enabled. If an empty string is provided, it will default
+     *                to a system-defined string (e.g. "Device is securely locked remotely.")
+     */
+    public EnableSecureLockDeviceParams(@NonNull String message) {
+        mMessage = message;
+    }
+
+    private EnableSecureLockDeviceParams(@NonNull Parcel in) {
+        mMessage = Objects.requireNonNull(in.readString8());
+    }
+
+    public static final @NonNull Creator<EnableSecureLockDeviceParams> CREATOR =
+            new Creator<EnableSecureLockDeviceParams>() {
+                @Override
+                public EnableSecureLockDeviceParams createFromParcel(Parcel in) {
+                    return new EnableSecureLockDeviceParams(in);
+                }
+
+                @Override
+                public EnableSecureLockDeviceParams[] newArray(int size) {
+                    return new EnableSecureLockDeviceParams[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mMessage);
+    }
+}
diff --git a/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl b/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl
new file mode 100644
index 0000000..5ad4534
--- /dev/null
+++ b/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.authenticationpolicy;
+
+import android.security.authenticationpolicy.EnableSecureLockDeviceParams;
+import android.security.authenticationpolicy.DisableSecureLockDeviceParams;
+
+/**
+ * Communication channel from AuthenticationPolicyManager to AuthenticationPolicyService.
+ * @hide
+ */
+interface IAuthenticationPolicyService {
+    @EnforcePermission("MANAGE_SECURE_LOCK_DEVICE")
+    int enableSecureLockDevice(in EnableSecureLockDeviceParams params);
+
+    @EnforcePermission("MANAGE_SECURE_LOCK_DEVICE")
+    int disableSecureLockDevice(in DisableSecureLockDeviceParams params);
+}
\ No newline at end of file
diff --git a/core/java/android/security/authenticationpolicy/OWNERS b/core/java/android/security/authenticationpolicy/OWNERS
new file mode 100644
index 0000000..4310d1a
--- /dev/null
+++ b/core/java/android/security/authenticationpolicy/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/security/authenticationpolicy/OWNERS
\ No newline at end of file
diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl
deleted file mode 100644
index 8039b26..0000000
--- a/core/java/android/security/forensic/IForensicService.aidl
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 android.security.forensic;
-
-import android.security.forensic.IForensicServiceCommandCallback;
-import android.security.forensic.IForensicServiceStateCallback;
-
-/**
- * Binder interface to communicate with ForensicService.
- * @hide
- */
-interface IForensicService {
-    @EnforcePermission("READ_FORENSIC_STATE")
-    void addStateCallback(IForensicServiceStateCallback callback);
-    @EnforcePermission("READ_FORENSIC_STATE")
-    void removeStateCallback(IForensicServiceStateCallback callback);
-    @EnforcePermission("MANAGE_FORENSIC_STATE")
-    void enable(IForensicServiceCommandCallback callback);
-    @EnforcePermission("MANAGE_FORENSIC_STATE")
-    void disable(IForensicServiceCommandCallback callback);
-}
diff --git a/core/java/android/security/forensic/IForensicEventTransport.aidl b/core/java/android/security/intrusiondetection/IIntrusionDetectionEventTransport.aidl
similarity index 67%
rename from core/java/android/security/forensic/IForensicEventTransport.aidl
rename to core/java/android/security/intrusiondetection/IIntrusionDetectionEventTransport.aidl
index 80e78eb..8759f72 100644
--- a/core/java/android/security/forensic/IForensicEventTransport.aidl
+++ b/core/java/android/security/intrusiondetection/IIntrusionDetectionEventTransport.aidl
@@ -14,25 +14,25 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
-import android.security.forensic.ForensicEvent;
+package android.security.intrusiondetection;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
 
 import com.android.internal.infra.AndroidFuture;
 
 /** {@hide} */
-oneway interface IForensicEventTransport {
+oneway interface IIntrusionDetectionEventTransport {
     /**
      * Initialize the server side.
      */
     void initialize(in AndroidFuture<int> resultFuture);
 
     /**
-     * Send forensic logging data to the backup destination.
-     * The data is a list of ForensicEvent.
-     * The ForensicEvent is an abstract class that represents
+     * Send intrusiondetection logging data to the backup destination.
+     * The data is a list of IntrusionDetectionEvent.
+     * The IntrusionDetectionEvent is an abstract class that represents
      * different type of events.
      */
-    void addData(in List<ForensicEvent> events, in AndroidFuture<int> resultFuture);
+    void addData(in List<IntrusionDetectionEvent> events, in AndroidFuture<int> resultFuture);
 
     /**
      * Release the binder to the server.
diff --git a/core/java/android/security/intrusiondetection/IIntrusionDetectionService.aidl b/core/java/android/security/intrusiondetection/IIntrusionDetectionService.aidl
new file mode 100644
index 0000000..0ba9418
--- /dev/null
+++ b/core/java/android/security/intrusiondetection/IIntrusionDetectionService.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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 android.security.intrusiondetection;
+
+import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback;
+import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback;
+
+/**
+ * Binder interface to communicate with IntrusionDetectionService.
+ * @hide
+ */
+interface IIntrusionDetectionService {
+    @EnforcePermission("READ_INTRUSION_DETECTION_STATE")
+    void addStateCallback(IIntrusionDetectionServiceStateCallback callback);
+    @EnforcePermission("READ_INTRUSION_DETECTION_STATE")
+    void removeStateCallback(IIntrusionDetectionServiceStateCallback callback);
+    @EnforcePermission("MANAGE_INTRUSION_DETECTION_STATE")
+    void enable(IIntrusionDetectionServiceCommandCallback callback);
+    @EnforcePermission("MANAGE_INTRUSION_DETECTION_STATE")
+    void disable(IIntrusionDetectionServiceCommandCallback callback);
+}
diff --git a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl b/core/java/android/security/intrusiondetection/IIntrusionDetectionServiceCommandCallback.aidl
similarity index 89%
rename from core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
rename to core/java/android/security/intrusiondetection/IIntrusionDetectionServiceCommandCallback.aidl
index 6d1456e..80f09d5 100644
--- a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
+++ b/core/java/android/security/intrusiondetection/IIntrusionDetectionServiceCommandCallback.aidl
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.security.intrusiondetection;
 
 /**
  * @hide
  */
- oneway interface IForensicServiceCommandCallback {
+ oneway interface IIntrusionDetectionServiceCommandCallback {
      @Backing(type="int")
      enum ErrorCode{
          UNKNOWN = 0,
diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/intrusiondetection/IIntrusionDetectionServiceStateCallback.aidl
similarity index 87%
rename from core/java/android/security/forensic/IForensicServiceStateCallback.aidl
rename to core/java/android/security/intrusiondetection/IIntrusionDetectionServiceStateCallback.aidl
index 1b68c7b..c88dc21 100644
--- a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
+++ b/core/java/android/security/intrusiondetection/IIntrusionDetectionServiceStateCallback.aidl
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.security.intrusiondetection;
 
 /**
  * @hide
  */
- oneway interface IForensicServiceStateCallback {
+ oneway interface IIntrusionDetectionServiceStateCallback {
     @Backing(type="int")
     enum State{
         UNKNOWN = 0,
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.aidl
similarity index 88%
rename from core/java/android/security/forensic/ForensicEvent.aidl
rename to core/java/android/security/intrusiondetection/IntrusionDetectionEvent.aidl
index a321fb0..80b4396 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.security.intrusiondetection;
 
 /** {@hide} */
-parcelable ForensicEvent;
+parcelable IntrusionDetectionEvent;
diff --git a/core/java/android/security/forensic/ForensicEvent.java b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
similarity index 80%
rename from core/java/android/security/forensic/ForensicEvent.java
rename to core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
index 3d908cc..538acf9 100644
--- a/core/java/android/security/forensic/ForensicEvent.java
+++ b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.security.intrusiondetection;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -30,12 +30,12 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * A class that represents a forensic event.
+ * A class that represents a intrusiondetection event.
  * @hide
  */
 @FlaggedApi(Flags.FLAG_AFL_API)
-public final class ForensicEvent implements Parcelable {
-    private static final String TAG = "ForensicEvent";
+public final class IntrusionDetectionEvent implements Parcelable {
+    private static final String TAG = "IntrusionDetectionEvent";
 
     public static final int SECURITY_EVENT = 0;
     public static final int NETWORK_EVENT_DNS = 1;
@@ -44,9 +44,9 @@
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
-        ForensicEvent.SECURITY_EVENT,
-        ForensicEvent.NETWORK_EVENT_DNS,
-        ForensicEvent.NETWORK_EVENT_CONNECT,
+        IntrusionDetectionEvent.SECURITY_EVENT,
+        IntrusionDetectionEvent.NETWORK_EVENT_DNS,
+        IntrusionDetectionEvent.NETWORK_EVENT_CONNECT,
     })
     public @interface EventType {}
 
@@ -56,39 +56,39 @@
     private final DnsEvent mNetworkEventDns;
     private final ConnectEvent mNetworkEventConnect;
 
-    public static final @NonNull Parcelable.Creator<ForensicEvent> CREATOR =
+    public static final @NonNull Parcelable.Creator<IntrusionDetectionEvent> CREATOR =
             new Parcelable.Creator<>() {
-                public ForensicEvent createFromParcel(Parcel in) {
-                    return new ForensicEvent(in);
+                public IntrusionDetectionEvent createFromParcel(Parcel in) {
+                    return new IntrusionDetectionEvent(in);
                 }
 
-                public ForensicEvent[] newArray(int size) {
-                    return new ForensicEvent[size];
+                public IntrusionDetectionEvent[] newArray(int size) {
+                    return new IntrusionDetectionEvent[size];
                 }
             };
 
-    public ForensicEvent(@NonNull SecurityEvent securityEvent) {
+    public IntrusionDetectionEvent(@NonNull SecurityEvent securityEvent) {
         mType = SECURITY_EVENT;
         mSecurityEvent = securityEvent;
         mNetworkEventDns = null;
         mNetworkEventConnect = null;
     }
 
-    public ForensicEvent(@NonNull DnsEvent dnsEvent) {
+    public IntrusionDetectionEvent(@NonNull DnsEvent dnsEvent) {
         mType = NETWORK_EVENT_DNS;
         mNetworkEventDns = dnsEvent;
         mSecurityEvent = null;
         mNetworkEventConnect = null;
     }
 
-    public ForensicEvent(@NonNull ConnectEvent connectEvent) {
+    public IntrusionDetectionEvent(@NonNull ConnectEvent connectEvent) {
         mType = NETWORK_EVENT_CONNECT;
         mNetworkEventConnect = connectEvent;
         mSecurityEvent = null;
         mNetworkEventDns = null;
     }
 
-    private ForensicEvent(@NonNull Parcel in) {
+    private IntrusionDetectionEvent(@NonNull Parcel in) {
         mType = in.readInt();
         switch (mType) {
             case SECURITY_EVENT:
@@ -111,7 +111,7 @@
         }
     }
 
-    /** Returns the type of the forensic event. */
+    /** Returns the type of the IntrusionDetectionEvent. */
     @NonNull
     public @EventType int getType() {
         return mType;
@@ -170,7 +170,7 @@
 
     @Override
     public String toString() {
-        return "ForensicEvent{"
+        return "IntrusionDetectionEvent{"
                 + "mType=" + mType
                 + '}';
     }
diff --git a/core/java/android/security/forensic/ForensicManager.java b/core/java/android/security/intrusiondetection/IntrusionDetectionManager.java
similarity index 62%
rename from core/java/android/security/forensic/ForensicManager.java
rename to core/java/android/security/intrusiondetection/IntrusionDetectionManager.java
index 9126182..e246338 100644
--- a/core/java/android/security/forensic/ForensicManager.java
+++ b/core/java/android/security/intrusiondetection/IntrusionDetectionManager.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.security.intrusiondetection;
 
-import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
-import static android.Manifest.permission.READ_FORENSIC_STATE;
+import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE;
+import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
@@ -41,23 +41,23 @@
 import java.util.function.Consumer;
 
 /**
- * ForensicManager manages the forensic logging on Android devices.
- * Upon user consent, forensic logging collects various device events for
+ * IntrusionDetectionManager manages the intrusion detection on Android devices.
+ * Upon user consent, intrusion detection collects various device events for
  * off-device investigation of potential device compromise.
  * <p>
- * Forensic logging can either be enabled ({@link #STATE_ENABLED}
+ * Intrusion detection logging can either be enabled ({@link #STATE_ENABLED}
  * or disabled ({@link #STATE_DISABLED}).
  * <p>
- * The Forensic logs will be transferred to
- * {@link android.security.forensic.ForensicEventTransport}.
+ * The intrusion detection logs will be transferred to
+ * {@link android.security.intrusiondetection.IntrusionDetectionEventTransport}.
  *
  * @hide
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_AFL_API)
-@SystemService(Context.FORENSIC_SERVICE)
-public class ForensicManager {
-    private static final String TAG = "ForensicManager";
+@SystemService(Context.INTRUSION_DETECTION_SERVICE)
+public class IntrusionDetectionManager {
+    private static final String TAG = "IntrusionDetectionManager";
 
     /** @hide */
     @Target(ElementType.TYPE_USE)
@@ -67,7 +67,7 @@
             STATE_DISABLED,
             STATE_ENABLED
     })
-    public @interface ForensicState {}
+    public @interface IntrusionDetectionState {}
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -77,64 +77,65 @@
             ERROR_TRANSPORT_UNAVAILABLE,
             ERROR_DATA_SOURCE_UNAVAILABLE
     })
-    public @interface ForensicError {}
+    public @interface IntrusionDetectionError {}
 
     /**
      * Indicates an unknown state
      */
-    public static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
+    public static final int STATE_UNKNOWN = IIntrusionDetectionServiceStateCallback.State.UNKNOWN;
 
     /**
-     * Indicates an state that the forensic is turned off.
+     * Indicates an state that the intrusion detection is turned off.
      */
-    public static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
+    public static final int STATE_DISABLED = IIntrusionDetectionServiceStateCallback.State.DISABLED;
 
     /**
-     * Indicates an state that the forensic is turned on.
+     * Indicates an state that the intrusion detection is turned on.
      */
-    public static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
+    public static final int STATE_ENABLED = IIntrusionDetectionServiceStateCallback.State.ENABLED;
 
     /**
      * Indicates an unknown error
      */
-    public static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
+    public static final int ERROR_UNKNOWN =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.UNKNOWN;
 
     /**
      * Indicates an error due to insufficient access rights.
      */
     public static final int ERROR_PERMISSION_DENIED =
-            IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
 
     /**
-     * Indicates an error due to unavailability of the forensic event transport.
+     * Indicates an error due to unavailability of the intrusion detection event transport.
      */
     public static final int ERROR_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
 
     /**
      * Indicates an error due to unavailability of the data source.
      */
     public static final int ERROR_DATA_SOURCE_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
 
 
-    private final IForensicService mService;
+    private final IIntrusionDetectionService mService;
 
-    private final ConcurrentHashMap<Consumer<Integer>, IForensicServiceStateCallback>
+    private final ConcurrentHashMap<Consumer<Integer>, IIntrusionDetectionServiceStateCallback>
             mStateCallbacks = new ConcurrentHashMap<>();
 
     /**
      * Constructor
      *
-     * @param service A valid instance of IForensicService.
+     * @param service A valid instance of IIntrusionDetectionService.
      * @hide
      */
-    public ForensicManager(IForensicService service) {
+    public IntrusionDetectionManager(IIntrusionDetectionService service) {
         mService = service;
     }
 
     /**
-     * Add a callback to monitor the state of the ForensicService.
+     * Add a callback to monitor the state of the IntrusionDetectionService.
      *
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback for state change.
@@ -142,9 +143,9 @@
      *                 to reflect the init state.
      *                 The callback can be registered only once.
      */
-    @RequiresPermission(READ_FORENSIC_STATE)
+    @RequiresPermission(READ_INTRUSION_DETECTION_STATE)
     public void addStateCallback(@NonNull @CallbackExecutor Executor executor,
-            @NonNull @ForensicState Consumer<Integer> callback) {
+            @NonNull @IntrusionDetectionState Consumer<Integer> callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
@@ -153,8 +154,8 @@
             return;
         }
 
-        final IForensicServiceStateCallback wrappedCallback =
-                new IForensicServiceStateCallback.Stub() {
+        final IIntrusionDetectionServiceStateCallback wrappedCallback =
+                new IIntrusionDetectionServiceStateCallback.Stub() {
                     @Override
                     public void onStateChange(int state) {
                         executor.execute(() -> callback.accept(state));
@@ -170,19 +171,19 @@
     }
 
     /**
-     * Remove a callback to monitor the state of the ForensicService.
+     * Remove a callback to monitor the state of the IntrusionDetectionService.
      *
      * @param callback The callback to remove.
      */
-    @RequiresPermission(READ_FORENSIC_STATE)
-    public void removeStateCallback(@NonNull Consumer<@ForensicState Integer> callback) {
+    @RequiresPermission(READ_INTRUSION_DETECTION_STATE)
+    public void removeStateCallback(@NonNull Consumer<@IntrusionDetectionState Integer> callback) {
         Objects.requireNonNull(callback);
         if (!mStateCallbacks.containsKey(callback)) {
             Log.d(TAG, "removeStateCallback callback not present");
             return;
         }
 
-        IForensicServiceStateCallback wrappedCallback = mStateCallbacks.get(callback);
+        IIntrusionDetectionServiceStateCallback wrappedCallback = mStateCallbacks.get(callback);
 
         try {
             mService.removeStateCallback(wrappedCallback);
@@ -194,22 +195,23 @@
     }
 
     /**
-     * Enable forensic logging.
-     * If successful, ForensicService will transition to {@link #STATE_ENABLED} state.
+     * Enable intrusion detection.
+     * If successful, IntrusionDetectionService will transition to {@link #STATE_ENABLED} state.
      * <p>
-     * When forensic logging is enabled, various device events will be collected and
-     * sent over to the registered {@link android.security.forensic.ForensicEventTransport}.
+     * When intrusion detection is enabled, various device events will be collected and
+     * sent over to the registered
+     * {@link android.security.intrusiondetection.IntrusionDetectionEventTransport}.
      *
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback for the command result.
      */
-    @RequiresPermission(MANAGE_FORENSIC_STATE)
+    @RequiresPermission(MANAGE_INTRUSION_DETECTION_STATE)
     public void enable(@NonNull @CallbackExecutor Executor executor,
             @NonNull CommandCallback callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
         try {
-            mService.enable(new IForensicServiceCommandCallback.Stub() {
+            mService.enable(new IIntrusionDetectionServiceCommandCallback.Stub() {
                 @Override
                 public void onSuccess() {
                     executor.execute(callback::onSuccess);
@@ -226,23 +228,23 @@
     }
 
     /**
-     * Disable forensic logging.
-     * If successful, ForensicService will transition to {@link #STATE_DISABLED}.
+     * Disable intrusion detection.
+     * If successful, IntrusionDetectionService will transition to {@link #STATE_DISABLED}.
      * <p>
-     * When forensic logging is disabled, device events will no longer be collected.
-     * Any events that have been collected but not yet sent to ForensicEventTransport
+     * When intrusion detection is disabled, device events will no longer be collected.
+     * Any events that have been collected but not yet sent to IntrusionDetectionEventTransport
      * will be transferred as a final batch.
      *
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback for the command result.
      */
-    @RequiresPermission(MANAGE_FORENSIC_STATE)
+    @RequiresPermission(MANAGE_INTRUSION_DETECTION_STATE)
     public void disable(@NonNull @CallbackExecutor Executor executor,
             @NonNull CommandCallback callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
         try {
-            mService.disable(new IForensicServiceCommandCallback.Stub() {
+            mService.disable(new IIntrusionDetectionServiceCommandCallback.Stub() {
                 @Override
                 public void onSuccess() {
                     executor.execute(callback::onSuccess);
@@ -271,6 +273,6 @@
          * Called when command fails.
          * @param error The error number.
          */
-        void onFailure(@ForensicError int error);
+        void onFailure(@IntrusionDetectionError int error);
     }
 }
diff --git a/core/java/android/security/forensic/OWNERS b/core/java/android/security/intrusiondetection/OWNERS
similarity index 100%
rename from core/java/android/security/forensic/OWNERS
rename to core/java/android/security/intrusiondetection/OWNERS
diff --git a/core/java/android/service/carrier/CarrierMessagingService.java b/core/java/android/service/carrier/CarrierMessagingService.java
index 61213e6..a825a7e 100644
--- a/core/java/android/service/carrier/CarrierMessagingService.java
+++ b/core/java/android/service/carrier/CarrierMessagingService.java
@@ -16,6 +16,7 @@
 
 package android.service.carrier;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +27,8 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
+import com.android.internal.telephony.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -97,16 +100,317 @@
     public static final int SEND_STATUS_RETRY_ON_CARRIER_NETWORK = 1;
 
     /**
-     * SMS/MMS sending failed. We should not retry via the carrier network.
+     * SMS/MMS sending failed due to an unspecified issue. Sending will not be retried via the
+     * carrier network.
+     *
+     * <p>Maps to SmsManager.RESULT_RIL_GENERIC_FAILURE for SMS and SmsManager.MMS_ERROR_UNSPECIFIED
+     * for MMS.
      */
     public static final int SEND_STATUS_ERROR = 2;
 
+    /**
+     * More precise error reasons for outbound SMS send requests. These will not be retried on the
+     * carrier network.
+     *
+     * <p>Each code maps directly to an SmsManager code (e.g. SEND_STATS_RESULT_ERROR_NULL_PDU maps
+     * to SmsManager.RESULT_ERROR_NULL_PDU).
+     */
+
+    /**
+     * Generic failure cause.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE = 200;
+
+    /**
+     * Failed because no pdu provided.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_NULL_PDU
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_NULL_PDU = 201;
+
+    /**
+     * Failed because service is currently unavailable.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_NO_SERVICE = 202;
+
+    /**
+     * Failed because we reached the sending queue limit.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED = 203;
+
+    /**
+     * Failed because FDN is enabled.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE = 204;
+
+    /**
+     * Failed because user denied the sending of this short code.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED = 205;
+
+    /**
+     * Failed because the user has denied this app ever send premium short codes.
+     *
+     * @see android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED = 206;
+
+    /**
+     * Failed because of network rejection.
+     *
+     * @see android.telephony.SmsManager.RESULT_NETWORK_REJECT
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_NETWORK_REJECT = 207;
+
+    /**
+     * Failed because of invalid arguments.
+     *
+     * @see android.telephony.SmsManager.RESULT_INVALID_ARGUMENTS
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_INVALID_ARGUMENTS = 208;
+
+    /**
+     * Failed because of an invalid state.
+     *
+     * @see android.telephony.SmsManager.RESULT_INVALID_STATE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_INVALID_STATE = 209;
+
+    /**
+     * Failed because the sms format is not valid.
+     *
+     * @see android.telephony.SmsManager.RESULT_INVALID_SMS_FORMAT
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_INVALID_SMS_FORMAT = 210;
+
+    /**
+     * Failed because of a network error.
+     *
+     * @see android.telephony.SmsManager.RESULT_NETWORK_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_NETWORK_ERROR = 211;
+
+    /**
+     * Failed because of an encoding error.
+     *
+     * @see android.telephony.SmsManager.RESULT_ENCODING_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_ENCODING_ERROR = 212;
+
+    /**
+     * Failed because of an invalid smsc address
+     *
+     * @see android.telephony.SmsManager.RESULT_INVALID_SMSC_ADDRESS
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS = 213;
+
+    /**
+     * Failed because the operation is not allowed.
+     *
+     * @see android.telephony.SmsManager.RESULT_OPERATION_NOT_ALLOWED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED = 214;
+
+    /**
+     * Failed because the operation was cancelled.
+     *
+     * @see android.telephony.SmsManager.RESULT_CANCELLED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_CANCELLED = 215;
+
+    /**
+     * Failed because the request is not supported.
+     *
+     * @see android.telephony.SmsManager.RESULT_REQUEST_NOT_SUPPORTED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED = 216;
+
+    /**
+     * Failed sending during an emergency call.
+     *
+     * @see android.telephony.SmsManager.RESULT_SMS_BLOCKED_DURING_EMERGENCY
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY = 217;
+
+    /**
+     * Failed to send an sms retry.
+     *
+     * @see android.telephony.SmsManager.RESULT_SMS_SEND_RETRY_FAILED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED = 218;
+
+    /**
+     * More precise error reasons for outbound MMS send requests. These will not be retried on the
+     * carrier network.
+     *
+     * <p>Each code maps directly to an SmsManager code (e.g.
+     * SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS maps to SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS).
+     */
+
+    /**
+     * Unspecific MMS error occurred during send.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_UNSPECIFIED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_UNSPECIFIED = 400;
+
+    /**
+     * ApnException occurred during MMS network setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INVALID_APN
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_INVALID_APN = 401;
+
+    /**
+     * An error occurred during the MMS connection setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 402;
+
+    /**
+     * An error occurred during the HTTP client setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_HTTP_FAILURE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_HTTP_FAILURE = 403;
+
+    /**
+     * An I/O error occurred reading the PDU.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_IO_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_IO_ERROR = 404;
+
+    /**
+     * An error occurred while retrying sending the MMS.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_RETRY
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_RETRY = 405;
+
+    /**
+     * The carrier-dependent configuration values could not be loaded.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_CONFIGURATION_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 406;
+
+    /**
+     * There is neither Wi-Fi nor mobile data network.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_NO_DATA_NETWORK
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK = 407;
+
+    /**
+     * The subscription id for the send is invalid.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 408;
+
+    /**
+     * The subscription id for the send is inactive.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 409;
+
+    /**
+     * Data is disabled for the MMS APN.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_DATA_DISABLED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_DATA_DISABLED = 410;
+
+    /**
+     * MMS is disabled by a carrier.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_MMS_DISABLED_BY_CARRIER
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 411;
+
     /** @hide */
-    @IntDef(prefix = { "SEND_STATUS_" }, value = {
-            SEND_STATUS_OK,
-            SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
-            SEND_STATUS_ERROR
-    })
+    @IntDef(
+            prefix = {"SEND_STATUS_"},
+            value = {
+                SEND_STATUS_OK,
+                SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                SEND_STATUS_ERROR,
+                SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE,
+                SEND_STATUS_RESULT_ERROR_NULL_PDU,
+                SEND_STATUS_RESULT_ERROR_NO_SERVICE,
+                SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED,
+                SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE,
+                SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED,
+                SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED,
+                SEND_STATUS_RESULT_NETWORK_REJECT,
+                SEND_STATUS_RESULT_INVALID_ARGUMENTS,
+                SEND_STATUS_RESULT_INVALID_STATE,
+                SEND_STATUS_RESULT_INVALID_SMS_FORMAT,
+                SEND_STATUS_RESULT_NETWORK_ERROR,
+                SEND_STATUS_RESULT_ENCODING_ERROR,
+                SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS,
+                SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED,
+                SEND_STATUS_RESULT_CANCELLED,
+                SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED,
+                SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY,
+                SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED,
+                SEND_STATUS_MMS_ERROR_UNSPECIFIED,
+                SEND_STATUS_MMS_ERROR_INVALID_APN,
+                SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS,
+                SEND_STATUS_MMS_ERROR_HTTP_FAILURE,
+                SEND_STATUS_MMS_ERROR_IO_ERROR,
+                SEND_STATUS_MMS_ERROR_RETRY,
+                SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR,
+                SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK,
+                SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID,
+                SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION,
+                SEND_STATUS_MMS_ERROR_DATA_DISABLED,
+                SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER
+            })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SendResult {}
 
@@ -121,16 +425,138 @@
     public static final int DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK = 1;
 
     /**
-     * MMS downloading failed. We should not retry via the carrier network.
+     * MMS downloading failed due to an unspecified issue. Downloading will not be retried via the
+     * carrier network.
+     *
+     * <p>Maps to SmsManager.MMR_ERROR_UNSPECIFIED.
      */
     public static final int DOWNLOAD_STATUS_ERROR = 2;
 
+    /**
+     * More precise error reasons for inbound MMS download requests. These will not be retried on
+     * the carrier network.
+     *
+     * <p>Each code maps directly to an SmsManager code (e.g.
+     * DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS maps to
+     * SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS).
+     */
+
+    /**
+     * Unspecific MMS error occurred during download.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_UNSPECIFIED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED = 600;
+
+    /**
+     * ApnException occurred during MMS network setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INVALID_APN
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN = 601;
+
+    /**
+     * An error occurred during the MMS connection setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 602;
+
+    /**
+     * An error occurred during the HTTP client setup.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_HTTP_FAILURE
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE = 603;
+
+    /**
+     * An I/O error occurred reading the PDU.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_IO_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR = 604;
+
+    /**
+     * An error occurred while retrying downloading the MMS.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_RETRY
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_RETRY = 605;
+
+    /**
+     * The carrier-dependent configuration values could not be loaded.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_CONFIGURATION_ERROR
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 606;
+
+    /**
+     * There is neither Wi-Fi nor mobile data network.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_NO_DATA_NETWORK
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK = 607;
+
+    /**
+     * The subscription id for the download is invalid.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 608;
+
+    /**
+     * The subscription id for the download is inactive.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 609;
+
+    /**
+     * Data is disabled for the MMS APN.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_DATA_DISABLED
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED = 610;
+
+    /**
+     * MMS is disabled by a carrier.
+     *
+     * @see android.telephony.SmsManager.MMS_ERROR_MMS_DISABLED_BY_CARRIER
+     */
+    @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE)
+    public static final int DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 611;
+
     /** @hide */
-    @IntDef(prefix = { "DOWNLOAD_STATUS_" }, value = {
-            DOWNLOAD_STATUS_OK,
-            DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK,
-            DOWNLOAD_STATUS_ERROR
-    })
+    @IntDef(
+            prefix = {"DOWNLOAD_STATUS_"},
+            value = {
+                DOWNLOAD_STATUS_OK,
+                DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK,
+                DOWNLOAD_STATUS_ERROR,
+                DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED,
+                DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN,
+                DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS,
+                DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE,
+                DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR,
+                DOWNLOAD_STATUS_MMS_ERROR_RETRY,
+                DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR,
+                DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK,
+                DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID,
+                DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION,
+                DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED,
+                DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER
+            })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DownloadResult {}
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 24328eb..1388778 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2532,7 +2532,7 @@
 
     /** Returns whether the rule id corresponds to an implicit rule. */
     public static boolean isImplicitRuleId(@NonNull String ruleId) {
-        return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
+        return ruleId != null && ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
     }
 
     private static int[] tryParseHourAndMinute(String value) {
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index 264db4a..239da87 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -210,6 +210,9 @@
     /**
      * Registers a {@link OnJankDataListener} to receive jank classification data about rendered
      * frames.
+     * <p>
+     * Use {@link SurfaceControl.OnJankDataListenerRegistration#removeAfter} to unregister the
+     * listener.
      *
      * @param executor The executor on which the listener will be invoked.
      * @param listener The listener to add.
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index d56768d..68674dd 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -37,6 +37,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.Size;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.graphics.ColorSpace;
@@ -58,6 +59,7 @@
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
 import android.hardware.graphics.common.DisplayDecorationSupport;
+import android.media.quality.PictureProfileHandle;
 import android.opengl.EGLDisplay;
 import android.opengl.EGLSync;
 import android.os.Build;
@@ -234,7 +236,6 @@
             long nativeObject, float currentBufferRatio, float desiredRatio);
     private static native void nativeSetDesiredHdrHeadroom(long transactionObj,
             long nativeObject, float desiredRatio);
-
     private static native void nativeSetCachingHint(long transactionObj,
             long nativeObject, int cachingHint);
     private static native void nativeSetDamageRegion(long transactionObj, long nativeObject,
@@ -314,6 +315,11 @@
     private static native void nativeSetLuts(long transactionObj, long nativeObject,
             float[] buffers, int[] slots, int[] dimensions, int[] sizes, int[] samplingKeys);
     private static native void nativeEnableDebugLogCallPoints(long transactionObj);
+    private static native int nativeGetMaxPictureProfiles();
+    private static native void nativeSetPictureProfileId(long transactionObj,
+            long nativeObject, long pictureProfileId);
+    private static native void nativeSetContentPriority(long transactionObj, long nativeObject,
+            int priority);
 
     /**
      * Transforms that can be applied to buffers as they are displayed to a window.
@@ -598,8 +604,11 @@
         }
 
         /**
-         * Request a flush of any pending jank classification data. May cause the registered
-         * listener to be invoked inband.
+         * Request a flush of any pending jank classification data.
+         * <p>
+         * May cause the registered listener to be invoked inband. Since jank is tracked by the
+         * system compositor by surface, flushing the data on one listener, will also cause other
+         * listeners on the same surface to receive jank classification data.
          */
         public void flush() {
             nativeFlushJankData(mNativeObject);
@@ -2833,6 +2842,33 @@
     }
 
     /**
+     * Retrieve the maximum number of concurrent picture profiles allowed across all displays.
+     *
+     * A picture profile is assigned to a layer via:
+     * <ul>
+     *     <li>Picture processing via {@link MediaCodec.KEY_PICTURE_PROFILE}</li>
+     *     <li>Picture processing via {@link SurfaceControl.Transaction#setPictureProfileHandle}
+     *     </li>
+     * </ul>
+     *
+     * If the maximum number is exceeded, some layers will not receive profiles based on:
+     * <ul>
+     *     <li>The content priority assigned by the app</li>
+     *     <li>The system-determined priority of the app owning the layer</li>
+     * </ul>
+     *
+     * @see MediaCodec.KEY_PICTURE_PROFILE
+     * @see SurfaceControl.Transaction#setPictureProfileHandle
+     * @see SurfaceControl.Transaction#setContentPriority
+     *
+     * @hide
+     */
+    @IntRange(from = 0)
+    public static int getMaxPictureProfiles() {
+        return nativeGetMaxPictureProfiles();
+    }
+
+    /**
      * Interface to handle request to
      * {@link SurfaceControl.Transaction#addTransactionCommittedListener(Executor, TransactionCommittedListener)}
      */
@@ -4595,6 +4631,71 @@
         }
 
         /**
+         * Sets the desired picture profile handle for a layer.
+         * <p>
+         * A handle, retrieved from {@link MediaQualityManager#getProfileHandles}, which
+         * refers a set of parameters that are used to configure picture processing that is applied
+         * to all subsequent buffers to enhance the quality of their contents (e.g. gamma, color
+         * temperature, hue, saturation, etc.).
+         * <p>
+         * Setting a handle does not guarantee access to limited picture processing. The content
+         * priority for the  as well as the prominance of app to the current user experience plays a
+         * role in which layer(s) get access to the limited picture processing resources. A maximum
+         * number of {@link MediaQualityManager.getMaxPictureProfiles} can be applied at any given
+         * point in time.
+         *
+         * @param sc The SurfaceControl of the layer that should be updated
+         * @param handle The picture profile handle which refers to the set of desired parameters
+         *
+         * @see MediaQualityManager#getMaxPictureProfiles
+         * @see MediaQualityManager#getProfileHandles
+         * @see MediaCodec.KEY_PICTURE_PROFILE
+         * @see SurfaceControl.Transaction#setContentPriority
+         *
+         * @hide
+         */
+        @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
+        @SystemApi
+        public @NonNull Transaction setPictureProfileHandle(@NonNull SurfaceControl sc,
+                                                            @NonNull PictureProfileHandle handle) {
+            checkPreconditions(sc);
+
+            nativeSetPictureProfileId(mNativeObject, sc.mNativeObject, handle.getId());
+            return this;
+        }
+
+        /**
+         * Sets the importance the layer's contents has to the app's user experience.
+         * <p>
+         * When a two layers within the same app are competing for a limited rendering resource,
+         * the priority will determine which layer gets access to the resource. The lower the
+         * priority, the more likely the layer will get access to the resource.
+         * <p>
+         * Resources managed by this priority:
+         * <ul>
+         *     <li>Picture processing via {@link MediaCodec.KEY_PICTURE_PROFILE}</li>
+         *     <li>Picture processing via {@link SurfaceControl.Transaction#setPictureProfileHandle}
+         *     </li>
+         * </ul>
+         *
+         * @param sc The SurfaceControl of the layer that should be updated
+         * @param priority The priority this layer should have with respect to other layers in the
+         *                 app. The default priority is zero.
+         *
+         * @see MediaQualityManager#getMaxPictureProfiles
+         * @see MediaCodec.KEY_PICTURE_PROFILE
+         * @see SurfaceControl.Transaction#setPictureProfileHandle
+         */
+        @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
+        public @NonNull Transaction setContentPriority(@NonNull SurfaceControl sc,
+                                                       int priority) {
+            checkPreconditions(sc);
+
+            nativeSetContentPriority(mNativeObject, sc.mNativeObject, priority);
+            return this;
+        }
+
+        /**
          * Sets the caching hint for the layer. By default, the caching hint is
          * {@link CACHING_ENABLED}.
          *
diff --git a/core/java/android/view/SurfaceControlActivePicture.java b/core/java/android/view/SurfaceControlActivePicture.java
new file mode 100644
index 0000000..569159d
--- /dev/null
+++ b/core/java/android/view/SurfaceControlActivePicture.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.view;
+
+import android.annotation.NonNull;
+import android.media.quality.PictureProfileHandle;
+
+/**
+ * A record of a visible layer that is using picture processing.
+ * @hide
+ */
+public class SurfaceControlActivePicture {
+    private final int mLayerId;
+    private final int mOwnerUid;
+    private final @NonNull PictureProfileHandle mPictureProfileHandle;
+
+    /**
+     * Create a record of a visible layer that is using picture processing.
+     *
+     * @param layerId the layer that is using picture processing
+     * @param ownerUid the UID of the package that owns the layer
+     * @param handle the handle for the picture profile that configured the processing
+     */
+    private SurfaceControlActivePicture(int layerId, int ownerUid, PictureProfileHandle handle) {
+        mLayerId = layerId;
+        mOwnerUid = ownerUid;
+        mPictureProfileHandle = handle;
+    }
+
+    /** The layer that is using picture processing.  */
+    public int getLayerId() {
+        return mLayerId;
+    }
+
+    /** The UID of the package that owns the layer using picture processing. */
+    public int getOwnerUid() {
+        return mOwnerUid;
+    }
+
+    /** A handle that indicates which picture profile has configured the picture processing. */
+    public @NonNull PictureProfileHandle getPictureProfileHandle() {
+        return mPictureProfileHandle;
+    }
+}
diff --git a/core/java/android/view/SurfaceControlActivePictureListener.java b/core/java/android/view/SurfaceControlActivePictureListener.java
new file mode 100644
index 0000000..d7c5374
--- /dev/null
+++ b/core/java/android/view/SurfaceControlActivePictureListener.java
@@ -0,0 +1,69 @@
+/*
+ * 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 android.view;
+
+import android.annotation.RequiresPermission;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * Allows for the monitoring of visible layers that are using picture processing.
+ * @hide
+ */
+public abstract class SurfaceControlActivePictureListener {
+    private static final NativeAllocationRegistry sRegistry =
+            NativeAllocationRegistry.createMalloced(
+                    SurfaceControlActivePictureListener.class.getClassLoader(),
+                    nativeGetDestructor());
+
+    /**
+      * Callback when there are changes in the visible layers that are using picture processing.
+      *
+      * @param activePictures The visible layers that are using picture processing.
+      */
+    public abstract void onActivePicturesChanged(SurfaceControlActivePicture[] activePictures);
+
+    /**
+     * Start listening to changes in active pictures.
+     */
+    @RequiresPermission(android.Manifest.permission.OBSERVE_PICTURE_PROFILES)
+    public void startListening() {
+        synchronized (this) {
+            long nativePtr = nativeMakeAndStartListening();
+            mDestructor = sRegistry.registerNativeAllocation(this, nativePtr);
+        }
+    }
+
+    /**
+     * Stop listening to changes in active pictures.
+     */
+    @RequiresPermission(android.Manifest.permission.OBSERVE_PICTURE_PROFILES)
+    public void stopListening() {
+        final Runnable destructor;
+        synchronized (this) {
+            destructor = mDestructor;
+        }
+        if (destructor != null) {
+            destructor.run();
+        }
+    }
+
+    private native long nativeMakeAndStartListening();
+    private static native long nativeGetDestructor();
+
+    private Runnable mDestructor;
+}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index eebdeadc..c7d5a9f 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -253,6 +253,9 @@
     namespace: "lse_desktop_experience"
     description: "Enables enter desktop windowing transition & motion polish changes"
     bug: "369763947"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
 
 flag {
@@ -260,6 +263,9 @@
     namespace: "lse_desktop_experience"
     description: "Enables exit desktop windowing transition & motion polish changes"
     bug: "353650462"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
 
 flag {
@@ -343,6 +349,9 @@
     namespace: "lse_desktop_experience"
     description: "Enables custom transitions for alt-tab app launches in Desktop Mode."
     bug: "370735595"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
 
 flag {
@@ -350,6 +359,9 @@
     namespace: "lse_desktop_experience"
     description: "Enables custom transitions for app launches in Desktop Mode."
     bug: "375992828"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
 
 flag {
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 96b9dc7..bb47707 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -29,14 +29,6 @@
 
 flag {
     namespace: "window_surfaces"
-    name: "secure_window_state"
-    description: "Move SC secure flag to WindowState level"
-    is_fixed_read_only: true
-    bug: "308662081"
-}
-
-flag {
-    namespace: "window_surfaces"
     name: "trusted_presentation_listener_for_window"
     is_exported: true
     description: "Enable trustedPresentationListener on windows public API"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index ff69610..86bbeb8 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -200,6 +200,13 @@
 }
 
 flag {
+  name: "show_app_handle_large_screens"
+  namespace: "windowing_frontend"
+  description: "Show the app handle and the app menu also on large screens that don't enable desktop mode"
+  bug: "377689543"
+}
+
+flag {
   name: "filter_irrelevant_input_device_change"
   namespace: "windowing_frontend"
   description: "Recompute display configuration only for necessary input device changes"
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
index 244bb3d..b5d3895 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
+
 import java.util.List;
 
 /** Interface for the companion operations */
@@ -25,5 +27,5 @@
      * @param buffer data to read to create operation
      * @param operations command is to be added
      */
-    void read(WireBuffer buffer, List<Operation> operations);
+    void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 0761a24..370289a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.operations.ComponentValue;
+import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
 import com.android.internal.widget.remotecompose.core.operations.IntegerExpression;
 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
@@ -53,15 +54,16 @@
 
     private static final boolean DEBUG = false;
 
-    ArrayList<Operation> mOperations;
+    @NonNull ArrayList<Operation> mOperations = new ArrayList<>();
 
     @Nullable RootLayoutComponent mRootLayoutComponent = null;
 
-    RemoteComposeState mRemoteComposeState = new RemoteComposeState();
+    @NonNull RemoteComposeState mRemoteComposeState = new RemoteComposeState();
     @NonNull TimeVariables mTimeVariables = new TimeVariables();
     // Semantic version of the document
     @NonNull Version mVersion = new Version(0, 1, 0);
 
+    @Nullable
     String mContentDescription; // text description of the document (used for accessibility)
 
     long mRequiredCapabilities = 0L; // bitmask indicating needed capabilities of the player(unused)
@@ -74,19 +76,22 @@
 
     int mContentAlignment = RootContentBehavior.ALIGNMENT_CENTER;
 
-    RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
+    @NonNull RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
 
     private final HashMap<Long, IntegerExpression> mIntegerExpressions = new HashMap<>();
 
+    private final HashMap<Integer, FloatExpression> mFloatExpressions = new HashMap<>();
+
     private HashSet<Component> mAppliedTouchOperations = new HashSet<>();
 
     private int mLastId = 1; // last component id when inflating the file
 
+    @Nullable
     public String getContentDescription() {
         return mContentDescription;
     }
 
-    public void setContentDescription(String contentDescription) {
+    public void setContentDescription(@Nullable String contentDescription) {
         this.mContentDescription = contentDescription;
     }
 
@@ -116,19 +121,21 @@
         mRemoteComposeState.setWindowHeight(height);
     }
 
+    @NonNull
     public RemoteComposeBuffer getBuffer() {
         return mBuffer;
     }
 
-    public void setBuffer(RemoteComposeBuffer buffer) {
+    public void setBuffer(@NonNull RemoteComposeBuffer buffer) {
         this.mBuffer = buffer;
     }
 
+    @NonNull
     public RemoteComposeState getRemoteComposeState() {
         return mRemoteComposeState;
     }
 
-    public void setRemoteComposeState(RemoteComposeState remoteComposeState) {
+    public void setRemoteComposeState(@NonNull RemoteComposeState remoteComposeState) {
         this.mRemoteComposeState = remoteComposeState;
     }
 
@@ -171,7 +178,7 @@
      * @param h vertical dimension of the rendering area
      * @param scaleOutput will contain the computed scale factor
      */
-    public void computeScale(float w, float h, float[] scaleOutput) {
+    public void computeScale(float w, float h, @NonNull float[] scaleOutput) {
         float contentScaleX = 1f;
         float contentScaleY = 1f;
         if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
@@ -236,7 +243,11 @@
      * @param translateOutput will contain the computed translation
      */
     private void computeTranslate(
-            float w, float h, float contentScaleX, float contentScaleY, float[] translateOutput) {
+            float w,
+            float h,
+            float contentScaleX,
+            float contentScaleY,
+            @NonNull float[] translateOutput) {
         int horizontalContentAlignment = mContentAlignment & 0xF0;
         int verticalContentAlignment = mContentAlignment & 0xF;
         float translateX = 0f;
@@ -350,6 +361,22 @@
         }
     }
 
+    /**
+     * Execute an integer expression with the given id and put its value on the targetId
+     *
+     * @param expressionId the id of the integer expression
+     * @param targetId the id of the value to update with the expression
+     * @param context the current context
+     */
+    public void evaluateFloatExpression(
+            int expressionId, int targetId, @NonNull RemoteContext context) {
+        FloatExpression expression = mFloatExpressions.get(expressionId);
+        if (expression != null) {
+            float v = expression.evaluate(context);
+            context.overrideFloat(targetId, v);
+        }
+    }
+
     // ============== Haptic support ==================
     public interface HapticEngine {
         void haptic(int type);
@@ -375,7 +402,7 @@
 
     /** Callback interface for host actions */
     public interface ActionCallback {
-        void onAction(String name, Object value);
+        void onAction(@NonNull String name, Object value);
     }
 
     @NonNull HashSet<ActionCallback> mActionListeners = new HashSet<ActionCallback>();
@@ -386,7 +413,7 @@
      * @param name the action name
      * @param value a parameter to the action
      */
-    public void runNamedAction(String name, Object value) {
+    public void runNamedAction(@NonNull String name, Object value) {
         // TODO: we might add an interface to group all valid parameter types
         for (ActionCallback callback : mActionListeners) {
             callback.onAction(name, value);
@@ -398,7 +425,7 @@
      *
      * @param callback
      */
-    public void addActionCallback(ActionCallback callback) {
+    public void addActionCallback(@NonNull ActionCallback callback) {
         mActionListeners.add(callback);
     }
 
@@ -408,7 +435,7 @@
     }
 
     public interface ClickCallbacks {
-        void click(int id, String metadata);
+        void click(int id, @Nullable String metadata);
     }
 
     @NonNull HashSet<ClickCallbacks> mClickListeners = new HashSet<>();
@@ -429,21 +456,21 @@
 
     public static class ClickAreaRepresentation {
         int mId;
-        String mContentDescription;
+        @Nullable final String mContentDescription;
         float mLeft;
         float mTop;
         float mRight;
         float mBottom;
-        String mMetadata;
+        @Nullable final String mMetadata;
 
         public ClickAreaRepresentation(
                 int id,
-                String contentDescription,
+                @Nullable String contentDescription,
                 float left,
                 float top,
                 float right,
                 float bottom,
-                String metadata) {
+                @Nullable String metadata) {
             this.mId = id;
             this.mContentDescription = contentDescription;
             this.mLeft = left;
@@ -484,10 +511,11 @@
             return mId;
         }
 
-        public String getContentDescription() {
+        public @Nullable String getContentDescription() {
             return mContentDescription;
         }
 
+        @Nullable
         public String getMetadata() {
             return mMetadata;
         }
@@ -502,6 +530,10 @@
                 IntegerExpression expression = (IntegerExpression) op;
                 mIntegerExpressions.put((long) expression.mId, expression);
             }
+            if (op instanceof FloatExpression) {
+                FloatExpression expression = (FloatExpression) op;
+                mFloatExpressions.put(expression.mId, expression);
+            }
         }
         mOperations = inflateComponents(mOperations);
         mBuffer = buffer;
@@ -605,7 +637,8 @@
 
     @NonNull private HashMap<Integer, Component> mComponentMap = new HashMap<Integer, Component>();
 
-    private void registerVariables(RemoteContext context, @NonNull ArrayList<Operation> list) {
+    private void registerVariables(
+            @NonNull RemoteContext context, @NonNull ArrayList<Operation> list) {
         for (Operation op : list) {
             if (op instanceof VariableSupport) {
                 ((VariableSupport) op).updateVariables(context);
@@ -701,12 +734,12 @@
      */
     public void addClickArea(
             int id,
-            String contentDescription,
+            @Nullable String contentDescription,
             float left,
             float top,
             float right,
             float bottom,
-            String metadata) {
+            @Nullable String metadata) {
         mClickAreas.add(
                 new ClickAreaRepresentation(
                         id, contentDescription, left, top, right, bottom, metadata));
@@ -726,7 +759,7 @@
      *
      * @param callback called when a click area has been hit, passing the click are id and metadata.
      */
-    public void addClickListener(ClickCallbacks callback) {
+    public void addClickListener(@NonNull ClickCallbacks callback) {
         mClickListeners.add(callback);
     }
 
@@ -744,7 +777,7 @@
      * Passing a click event to the document. This will possibly result in calling the click
      * listeners.
      */
-    public void onClick(RemoteContext context, float x, float y) {
+    public void onClick(@NonNull RemoteContext context, float x, float y) {
         for (ClickAreaRepresentation clickArea : mClickAreas) {
             if (clickArea.contains(x, y)) {
                 warnClickListeners(clickArea);
@@ -802,6 +835,14 @@
         for (TouchListener clickArea : mTouchListeners) {
             clickArea.touchDrag(context, x, y);
         }
+        if (mRootLayoutComponent != null) {
+            for (Component component : mAppliedTouchOperations) {
+                component.onTouchDrag(context, this, x, y, true);
+            }
+            if (!mAppliedTouchOperations.isEmpty()) {
+                return true;
+            }
+        }
         if (!mTouchListeners.isEmpty()) {
             return true;
         }
@@ -940,6 +981,7 @@
      */
     public void paint(@NonNull RemoteContext context, int theme) {
         context.getPaintContext().clearNeedsRepaint();
+        context.loadFloat(RemoteContext.ID_DENSITY, context.getDensity());
         context.mMode = RemoteContext.ContextMode.UNSET;
         // current theme starts as UNSPECIFIED, until a Theme setter
         // operation gets executed and modify it.
@@ -1097,6 +1139,7 @@
         }
     }
 
+    @NonNull
     public List<Operation> getOperations() {
         return mOperations;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
index f1885f9..102003e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
@@ -15,22 +15,22 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
-import android.annotation.Nullable;
+import android.annotation.NonNull;
 
 /** Base interface for RemoteCompose operations */
 public interface Operation {
 
     /** add the operation to the buffer */
-    void write(WireBuffer buffer);
+    void write(@NonNull WireBuffer buffer);
 
     /**
      * paint an operation
      *
      * @param context the paint context used to paint the operation
      */
-    void apply(RemoteContext context);
+    void apply(@NonNull RemoteContext context);
 
     /** Debug utility to display an operation + indentation */
-    @Nullable
-    String deepToString(String indent);
+    @NonNull
+    String deepToString(@NonNull String indent);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index 53c45fa..687a99b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -98,7 +98,9 @@
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.OffsetModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ScrollModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ValueFloatChangeActionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ValueFloatExpressionChangeActionOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ValueIntegerChangeActionOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ValueIntegerExpressionChangeActionOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ValueStringChangeActionOperation;
@@ -214,6 +216,7 @@
     public static final int MODIFIER_OFFSET = 221;
     public static final int MODIFIER_ZINDEX = 223;
     public static final int MODIFIER_GRAPHICS_LAYER = 224;
+    public static final int MODIFIER_SCROLL = 226;
 
     public static final int LOOP_START = 215;
     public static final int LOOP_END = 216;
@@ -226,6 +229,7 @@
     public static final int VALUE_STRING_CHANGE_ACTION = 213;
     public static final int VALUE_INTEGER_EXPRESSION_CHANGE_ACTION = 218;
     public static final int VALUE_FLOAT_CHANGE_ACTION = 222;
+    public static final int VALUE_FLOAT_EXPRESSION_CHANGE_ACTION = 227;
 
     public static final int ANIMATION_SPEC = 14;
 
@@ -235,7 +239,7 @@
 
     static class UniqueIntMap<T> extends IntMap<T> {
         @Override
-        public T put(int key, T value) {
+        public T put(int key, @NonNull T value) {
             assert null == get(key) : "Opcode " + key + " already used in Operations !";
             return super.put(key, value);
         }
@@ -316,6 +320,7 @@
         map.put(MODIFIER_OFFSET, OffsetModifierOperation::read);
         map.put(MODIFIER_ZINDEX, ZIndexModifierOperation::read);
         map.put(MODIFIER_GRAPHICS_LAYER, GraphicsLayerModifierOperation::read);
+        map.put(MODIFIER_SCROLL, ScrollModifierOperation::read);
 
         map.put(OPERATIONS_LIST_END, OperationsListEnd::read);
 
@@ -327,6 +332,9 @@
                 ValueIntegerExpressionChangeActionOperation::read);
         map.put(VALUE_STRING_CHANGE_ACTION, ValueStringChangeActionOperation::read);
         map.put(VALUE_FLOAT_CHANGE_ACTION, ValueFloatChangeActionOperation::read);
+        map.put(
+                VALUE_FLOAT_EXPRESSION_CHANGE_ACTION,
+                ValueFloatExpressionChangeActionOperation::read);
 
         map.put(LAYOUT_ROOT, RootLayoutComponent::read);
         map.put(LAYOUT_CONTENT, LayoutComponentContent::read);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index 1a71afe..38b08e9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 
 /** Specify an abstract paint context used by RemoteCompose commands to draw */
@@ -22,9 +24,10 @@
     public static final int TEXT_MEASURE_MONOSPACE_WIDTH = 0x01;
     public static final int TEXT_MEASURE_FONT_HEIGHT = 0x02;
 
-    protected RemoteContext mContext;
+    protected @NonNull RemoteContext mContext;
     private boolean mNeedsRepaint = false;
 
+    @NonNull
     public RemoteContext getContext() {
         return mContext;
     }
@@ -37,11 +40,11 @@
         mNeedsRepaint = false;
     }
 
-    public PaintContext(RemoteContext context) {
+    public PaintContext(@NonNull RemoteContext context) {
         this.mContext = context;
     }
 
-    public void setContext(RemoteContext context) {
+    public void setContext(@NonNull RemoteContext context) {
         this.mContext = context;
     }
 
@@ -117,7 +120,8 @@
      *     descent of the font (not just of the measured text)
      * @param bounds the bounds (left, top, right, bottom)
      */
-    public abstract void getTextBounds(int textId, int start, int end, int flags, float[] bounds);
+    public abstract void getTextBounds(
+            int textId, int start, int end, int flags, @NonNull float[] bounds);
 
     /**
      * Draw a text starting ast x,y
@@ -158,7 +162,7 @@
      *
      * @param mPaintData the list of changes
      */
-    public abstract void applyPaint(PaintBundle mPaintData);
+    public abstract void applyPaint(@NonNull PaintBundle mPaintData);
 
     /**
      * Scale the rendering by scaleX and saleY (1.0 = no scale). Scaling is done about
@@ -264,7 +268,7 @@
      *
      * @param content the content to log
      */
-    public void log(String content) {
+    public void log(@NonNull String content) {
         System.out.println("[LOG] " + content);
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
index 049e477..9999182 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
@@ -35,17 +35,17 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
-    public abstract void paint(PaintContext context);
+    public abstract void paint(@NonNull PaintContext context);
 
     /**
      * Will return true if the operation is similar enough to the current one, in the context of an
      * animated transition.
      */
-    public boolean suitableForTransition(Operation op) {
+    public boolean suitableForTransition(@NonNull Operation op) {
         // by default expects the op to not be suitable
         return false;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Platform.java b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
index 7fbcfae..dcb8efeb 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Platform.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
@@ -15,19 +15,20 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 
 /** Services that are needed to be provided by the platform during encoding. */
 public interface Platform {
     @Nullable
-    byte[] imageToByteArray(Object image);
+    byte[] imageToByteArray(@NonNull Object image);
 
-    int getImageWidth(Object image);
+    int getImageWidth(@NonNull Object image);
 
-    int getImageHeight(Object image);
+    int getImageHeight(@NonNull Object image);
 
     @Nullable
-    float[] pathToFloatArray(Object path);
+    float[] pathToFloatArray(@NonNull Object path);
 
     enum LogCategory {
         DEBUG,
@@ -42,22 +43,22 @@
     Platform None =
             new Platform() {
                 @Override
-                public byte[] imageToByteArray(Object image) {
+                public byte[] imageToByteArray(@NonNull Object image) {
                     throw new UnsupportedOperationException();
                 }
 
                 @Override
-                public int getImageWidth(Object image) {
+                public int getImageWidth(@NonNull Object image) {
                     throw new UnsupportedOperationException();
                 }
 
                 @Override
-                public int getImageHeight(Object image) {
+                public int getImageHeight(@NonNull Object image) {
                     throw new UnsupportedOperationException();
                 }
 
                 @Override
-                public float[] pathToFloatArray(Object path) {
+                public float[] pathToFloatArray(@NonNull Object path) {
                     throw new UnsupportedOperationException();
                 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index 7d9439d..c05079e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import static com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression.ADD;
+import static com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression.MUL;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
@@ -89,6 +92,7 @@
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.OffsetModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ScrollModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ZIndexModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap;
@@ -117,9 +121,9 @@
     public static final int EASING_SPLINE_CUSTOM = FloatAnimation.SPLINE_CUSTOM;
     public static final int EASING_EASE_OUT_BOUNCE = FloatAnimation.EASE_OUT_BOUNCE;
     public static final int EASING_EASE_OUT_ELASTIC = FloatAnimation.EASE_OUT_ELASTIC;
-    WireBuffer mBuffer = new WireBuffer();
-    @Nullable Platform mPlatform = null;
-    RemoteComposeState mRemoteComposeState;
+    private @NonNull WireBuffer mBuffer = new WireBuffer();
+    @Nullable private Platform mPlatform = null;
+    @NonNull private final RemoteComposeState mRemoteComposeState;
     private static final boolean DEBUG = false;
 
     private int mLastComponentId = 0;
@@ -130,7 +134,7 @@
      *
      * @param remoteComposeState the state used while encoding on the buffer
      */
-    public RemoteComposeBuffer(RemoteComposeState remoteComposeState) {
+    public RemoteComposeBuffer(@NonNull RemoteComposeState remoteComposeState) {
         this.mRemoteComposeState = remoteComposeState;
     }
 
@@ -155,15 +159,15 @@
         return mPlatform;
     }
 
-    public void setPlatform(Platform platform) {
+    public void setPlatform(@NonNull Platform platform) {
         this.mPlatform = platform;
     }
 
-    public WireBuffer getBuffer() {
+    public @NonNull WireBuffer getBuffer() {
         return mBuffer;
     }
 
-    public void setBuffer(WireBuffer buffer) {
+    public void setBuffer(@NonNull WireBuffer buffer) {
         this.mBuffer = buffer;
     }
 
@@ -200,7 +204,7 @@
      * @param height the height of the document in pixels
      * @param contentDescription content description of the document
      */
-    public void header(int width, int height, String contentDescription) {
+    public void header(int width, int height, @Nullable String contentDescription) {
         header(width, height, contentDescription, 1f, 0);
     }
 
@@ -220,7 +224,7 @@
      * @param dstBottom bottom coordinate of the destination area
      */
     public void drawBitmap(
-            Object image,
+            @NonNull Object image,
             int imageWidth,
             int imageHeight,
             int srcLeft,
@@ -235,8 +239,9 @@
         int imageId = mRemoteComposeState.dataGetId(image);
         if (imageId == -1) {
             imageId = mRemoteComposeState.cacheData(image);
-            byte[] data = mPlatform.imageToByteArray(image);
-            BitmapData.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+            byte[] data = mPlatform.imageToByteArray(image); // todo: potential npe
+            BitmapData.apply(
+                    mBuffer, imageId, imageWidth, imageHeight, data); // todo: potential npe
         }
         int contentDescriptionId = 0;
         if (contentDescription != null) {
@@ -387,7 +392,7 @@
      * @param contentDescription content description of the image
      */
     public void addDrawBitmap(
-            Object image,
+            @NonNull Object image,
             float left,
             float top,
             float right,
@@ -396,11 +401,12 @@
         int imageId = mRemoteComposeState.dataGetId(image);
         if (imageId == -1) {
             imageId = mRemoteComposeState.cacheData(image);
-            byte[] data = mPlatform.imageToByteArray(image);
+            byte[] data = mPlatform.imageToByteArray(image); // todo: potential npe
             int imageWidth = mPlatform.getImageWidth(image);
             int imageHeight = mPlatform.getImageHeight(image);
 
-            BitmapData.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+            BitmapData.apply(
+                    mBuffer, imageId, imageWidth, imageHeight, data); // todo: potential npe
         }
         int contentDescriptionId = 0;
         if (contentDescription != null) {
@@ -446,7 +452,7 @@
      * @param contentDescription associate a string with image for accessibility
      */
     public void drawScaledBitmap(
-            Object image,
+            @NonNull Object image,
             float srcLeft,
             float srcTop,
             float srcRight,
@@ -461,11 +467,11 @@
         int imageId = mRemoteComposeState.dataGetId(image);
         if (imageId == -1) {
             imageId = mRemoteComposeState.cacheData(image);
-            byte[] data = mPlatform.imageToByteArray(image);
+            byte[] data = mPlatform.imageToByteArray(image); // todo: potential npe
             int imageWidth = mPlatform.getImageWidth(image);
             int imageHeight = mPlatform.getImageHeight(image);
 
-            BitmapData.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+            BitmapData.apply(mBuffer, imageId, imageWidth, imageHeight, data); // todo: potential pe
         }
         int contentDescriptionId = 0;
         if (contentDescription != null) {
@@ -493,11 +499,11 @@
      * @param image drawScaledBitmap
      * @return id of the image useful with
      */
-    public int addBitmap(Object image) {
+    public int addBitmap(@NonNull Object image) {
         int imageId = mRemoteComposeState.dataGetId(image);
         if (imageId == -1) {
             imageId = mRemoteComposeState.cacheData(image);
-            byte[] data = mPlatform.imageToByteArray(image);
+            byte[] data = mPlatform.imageToByteArray(image); // tODO: potential npe
             int imageWidth = mPlatform.getImageWidth(image);
             int imageHeight = mPlatform.getImageHeight(image);
 
@@ -512,11 +518,11 @@
      * @param image drawScaledBitmap
      * @return id of the image useful with
      */
-    public int addBitmap(Object image, @NonNull String name) {
+    public int addBitmap(@NonNull Object image, @NonNull String name) {
         int imageId = mRemoteComposeState.dataGetId(image);
         if (imageId == -1) {
             imageId = mRemoteComposeState.cacheData(image);
-            byte[] data = mPlatform.imageToByteArray(image);
+            byte[] data = mPlatform.imageToByteArray(image); // todo: potential npe
             int imageWidth = mPlatform.getImageWidth(image);
             int imageHeight = mPlatform.getImageHeight(image);
 
@@ -629,7 +635,7 @@
      *
      * @param path The path to be drawn
      */
-    public void addDrawPath(Object path) {
+    public void addDrawPath(@NonNull Object path) {
         int id = mRemoteComposeState.dataGetId(path);
         if (id == -1) { // never been seen before
             id = addPathData(path);
@@ -681,7 +687,8 @@
      * @param hOffset The distance along the path to add to the text's starting position
      * @param vOffset The distance above(-) or below(+) the path to position the text
      */
-    public void addDrawTextOnPath(@NonNull String text, Object path, float hOffset, float vOffset) {
+    public void addDrawTextOnPath(
+            @NonNull String text, @NonNull Object path, float hOffset, float vOffset) {
         int pathId = mRemoteComposeState.dataGetId(path);
         if (pathId == -1) { // never been seen before
             pathId = addPathData(path);
@@ -752,7 +759,7 @@
      *   <li>Panning of 1.0, 0.0 - the test is centered & to the right of x,y
      * </ul>
      *
-     * Setting panY to NaN results in y being the baseline of the text.
+     * <p>Setting panY to NaN results in y being the baseline of the text.
      *
      * @param text text to draw
      * @param x Coordinate of the Anchor
@@ -836,7 +843,7 @@
      *   <li>Panning of 1.0, 0.0 - the test is centered & to the right of x,y
      * </ul>
      *
-     * Setting panY to NaN results in y being the baseline of the text.
+     * <p>Setting panY to NaN results in y being the baseline of the text.
      *
      * @param textId text to draw
      * @param x Coordinate of the Anchor
@@ -861,7 +868,8 @@
      * @param start The start of the subrange of paths to draw 0 = start form start 0.5 is half way
      * @param stop The end of the subrange of paths to draw 1 = end at the end 0.5 is end half way
      */
-    public void addDrawTweenPath(Object path1, Object path2, float tween, float start, float stop) {
+    public void addDrawTweenPath(
+            @NonNull Object path1, @NonNull Object path2, float tween, float start, float stop) {
         int path1Id = mRemoteComposeState.dataGetId(path1);
         if (path1Id == -1) { // never been seen before
             path1Id = addPathData(path1);
@@ -892,7 +900,7 @@
      * @param path
      * @return the id of the path on the wire
      */
-    public int addPathData(Object path) {
+    public int addPathData(@NonNull Object path) {
         float[] pathData = mPlatform.pathToFloatArray(path);
         int id = mRemoteComposeState.cacheData(path);
         PathData.apply(mBuffer, id, pathData);
@@ -910,7 +918,7 @@
 
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void inflateFromBuffer(ArrayList<Operation> operations) {
+    public void inflateFromBuffer(@NonNull ArrayList<Operation> operations) {
         mBuffer.setIndex(0);
         while (mBuffer.available()) {
             int opId = mBuffer.readByte();
@@ -926,7 +934,7 @@
     }
 
     public static void readNextOperation(
-            @NonNull WireBuffer buffer, ArrayList<Operation> operations) {
+            @NonNull WireBuffer buffer, @NonNull ArrayList<Operation> operations) {
         int opId = buffer.readByte();
         if (DEBUG) {
             Utils.log(">> " + opId);
@@ -957,15 +965,16 @@
 
     @NonNull
     public static RemoteComposeBuffer fromFile(
-            @NonNull String path, RemoteComposeState remoteComposeState) throws IOException {
+            @NonNull String path, @NonNull RemoteComposeState remoteComposeState)
+            throws IOException {
         RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
         read(new File(path), buffer);
         return buffer;
     }
 
     @NonNull
-    public RemoteComposeBuffer fromFile(@NonNull File file, RemoteComposeState remoteComposeState)
-            throws IOException {
+    public RemoteComposeBuffer fromFile(
+            @NonNull File file, @NonNull RemoteComposeState remoteComposeState) throws IOException {
         RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
         read(file, buffer);
         return buffer;
@@ -973,7 +982,7 @@
 
     @NonNull
     public static RemoteComposeBuffer fromInputStream(
-            @NonNull InputStream inputStream, RemoteComposeState remoteComposeState) {
+            @NonNull InputStream inputStream, @NonNull RemoteComposeState remoteComposeState) {
         RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
         read(inputStream, buffer);
         return buffer;
@@ -1205,6 +1214,44 @@
     /**
      * Add a touch handle system
      *
+     * @param id the float NaN id used for the returned position
+     * @param value the default value
+     * @param min the minimum value
+     * @param max the maximum value
+     * @param velocityId the id for the velocity TODO support in v2
+     * @param exp The Float Expression
+     * @param touchMode the touch up handling behaviour
+     * @param touchSpec the touch up handling parameters
+     * @param easingSpec the easing parameter TODO support in v2
+     */
+    public void addTouchExpression(
+            float id,
+            float value,
+            float min,
+            float max,
+            float velocityId,
+            int touchEffects,
+            float[] exp,
+            int touchMode,
+            float[] touchSpec,
+            float[] easingSpec) {
+        TouchExpression.apply(
+                mBuffer,
+                Utils.idFromNan(id),
+                value,
+                min,
+                max,
+                velocityId,
+                touchEffects,
+                exp,
+                touchMode,
+                touchSpec,
+                easingSpec);
+    }
+
+    /**
+     * Add a touch handle system
+     *
      * @param value the default value
      * @param min the minimum value
      * @param max the maximum value
@@ -1225,9 +1272,8 @@
             int touchMode,
             float[] touchSpec,
             float[] easingSpec) {
-        int id = mRemoteComposeState.nextId();
-        TouchExpression.apply(
-                mBuffer,
+        float id = Utils.asNan(mRemoteComposeState.nextId());
+        addTouchExpression(
                 id,
                 value,
                 min,
@@ -1238,7 +1284,7 @@
                 touchMode,
                 touchSpec,
                 easingSpec);
-        return Utils.asNan(id);
+        return id;
     }
 
     /**
@@ -1248,7 +1294,7 @@
      * @param animation Array of floats that represents animation
      * @return NaN id of the result of the calculation
      */
-    public float addAnimatedFloat(@NonNull float[] value, float[] animation) {
+    public float addAnimatedFloat(@NonNull float[] value, @Nullable float[] animation) {
         int id = mRemoteComposeState.cacheData(value);
         FloatExpression.apply(mBuffer, id, value, animation);
         return Utils.asNan(id);
@@ -1345,7 +1391,7 @@
      * @param listId
      * @return the id of the map, encoded as a float NaN
      */
-    public int addMap(@NonNull String[] keys, byte[] types, int[] listId) {
+    public int addMap(@NonNull String[] keys, @Nullable byte[] types, @NonNull int[] listId) {
         int id = mRemoteComposeState.cacheData(listId, NanMap.TYPE_ARRAY);
         DataMapIds.apply(mBuffer, id, keys, types, listId);
         return id;
@@ -1354,14 +1400,18 @@
     /**
      * This provides access to text in RemoteList
      *
+     * <p>TODO: do we want both a float and an int index version of this method? bbade@ TODO
+     * for @hoford - add a unit test for this method
+     *
      * @param dataSet
      * @param index index as a float variable
      * @return
      */
     public int textLookup(float dataSet, float index) {
         long hash =
-                ((long) Float.floatToRawIntBits(dataSet))
-                        << (32 + Float.floatToRawIntBits(index)); // TODO: is this the correct ()s?
+                (((long) Float.floatToRawIntBits(dataSet)) << 32)
+                        + Float.floatToRawIntBits(
+                                index); // TODO: is this the correct ()s? -- bbade@
         int id = mRemoteComposeState.cacheData(hash);
         TextLookup.apply(mBuffer, id, Utils.idFromNan(dataSet), index);
         return id;
@@ -1370,14 +1420,16 @@
     /**
      * This provides access to text in RemoteList
      *
+     * <p>TODO for hoford - add a unit test for this method
+     *
      * @param dataSet
      * @param index index as an int variable
      * @return
      */
     public int textLookup(float dataSet, int index) {
         long hash =
-                ((long) Float.floatToRawIntBits(dataSet))
-                        << (32 + Float.floatToRawIntBits(index)); // TODO: is this the correct ()s?
+                (((long) Float.floatToRawIntBits(dataSet)) << 32)
+                        + Float.floatToRawIntBits(index); // TODO: is this the correct ()s?
         int id = mRemoteComposeState.cacheData(hash);
         TextLookupInt.apply(mBuffer, id, Utils.idFromNan(dataSet), index);
         return id;
@@ -1521,8 +1573,8 @@
      * @param wrap the wraps value so (e.g 360 so angles 355 would animate to 5)
      * @return
      */
-    public static float[] packAnimation(
-            float duration, int type, float[] spec, float initialValue, float wrap) {
+    public static @NonNull float[] packAnimation(
+            float duration, int type, @Nullable float[] spec, float initialValue, float wrap) {
 
         return FloatAnimation.packToFloatArray(duration, type, spec, initialValue, wrap);
     }
@@ -1591,6 +1643,43 @@
     }
 
     /**
+     * Add a scroll modifier
+     *
+     * @param direction HORIZONTAL(0) or VERTICAL(1)
+     * @param positionId the position id as a NaN
+     * @param notches
+     */
+    public void addModifierScroll(int direction, float positionId, int notches) {
+        // TODO: add support for non-notch behaviors etc.
+        float max = this.addFloat(0f);
+        float notchMax = this.addFloat(0f);
+        float touchExpressionDirection =
+                direction != 0 ? RemoteContext.FLOAT_TOUCH_POS_X : RemoteContext.FLOAT_TOUCH_POS_Y;
+        this.addTouchExpression(
+                positionId,
+                0f,
+                0f,
+                max,
+                0f,
+                3,
+                new float[] {
+                    touchExpressionDirection,
+                    -1,
+                    // TODO: remove this CONTINUOUS_SEC hack...
+                    MUL,
+                    RemoteContext.FLOAT_CONTINUOUS_SEC,
+                    0f,
+                    MUL,
+                    ADD
+                },
+                TouchExpression.STOP_NOTCHES_EVEN,
+                new float[] {notches, notchMax},
+                null);
+
+        ScrollModifierOperation.apply(mBuffer, direction, positionId, max, notchMax);
+    }
+
+    /**
      * Add a background modifier of provided color
      *
      * @param color the color of the background
@@ -1869,4 +1958,8 @@
     public int createID(int type) {
         return mRemoteComposeState.nextId(type);
     }
+
+    public int nextId() {
+        return mRemoteComposeState.nextId();
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
index 3039328..a903e6e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
@@ -49,7 +49,7 @@
     private final IntMap<Object> mObjectMap = new IntMap<>();
 
     private final boolean[] mColorOverride = new boolean[MAX_COLORS];
-    private final IntMap<ArrayAccess> mCollectionMap = new IntMap<>();
+    @NonNull private final IntMap<ArrayAccess> mCollectionMap = new IntMap<>();
 
     private final boolean[] mDataOverride = new boolean[MAX_DATA];
     private final boolean[] mIntegerOverride = new boolean[MAX_DATA];
@@ -82,7 +82,7 @@
     }
 
     /** Return the id of an item from the cache. */
-    public int dataGetId(Object data) {
+    public int dataGetId(@NonNull Object data) {
         Integer res = mDataIntMap.get(data);
         if (res == null) {
             return -1;
@@ -94,7 +94,7 @@
      * Add an item to the cache. Generates an id for the item and adds it to the cache based on that
      * id.
      */
-    public int cacheData(Object item) {
+    public int cacheData(@NonNull Object item) {
         int id = nextId();
         mDataIntMap.put(item, id);
         mIntDataMap.put(id, item);
@@ -105,7 +105,7 @@
      * Add an item to the cache. Generates an id for the item and adds it to the cache based on that
      * id.
      */
-    public int cacheData(Object item, int type) {
+    public int cacheData(@NonNull Object item, int type) {
         int id = nextId(type);
         mDataIntMap.put(item, id);
         mIntDataMap.put(id, item);
@@ -113,13 +113,13 @@
     }
 
     /** Insert an item in the cache */
-    public void cacheData(int id, Object item) {
+    public void cacheData(int id, @NonNull Object item) {
         mDataIntMap.put(item, id);
         mIntDataMap.put(id, item);
     }
 
     /** Insert an item in the cache */
-    public void updateData(int id, Object item) {
+    public void updateData(int id, @NonNull Object item) {
         if (!mDataOverride[id]) {
             Object previous = mIntDataMap.get(id);
             if (previous != item) {
@@ -137,7 +137,7 @@
      * @param id
      * @param item the new value
      */
-    public void overrideData(int id, Object item) {
+    public void overrideData(int id, @NonNull Object item) {
         Object previous = mIntDataMap.get(id);
         if (previous != item) {
             mDataIntMap.remove(previous);
@@ -379,7 +379,7 @@
     @NonNull IntMap<ArrayList<VariableSupport>> mVarListeners = new IntMap<>();
     @NonNull ArrayList<VariableSupport> mAllVarListeners = new ArrayList<>();
 
-    private void add(int id, VariableSupport variableSupport) {
+    private void add(int id, @NonNull VariableSupport variableSupport) {
         ArrayList<VariableSupport> v = mVarListeners.get(id);
         if (v == null) {
             v = new ArrayList<VariableSupport>();
@@ -395,17 +395,27 @@
      * @param id
      * @param variableSupport
      */
-    public void listenToVar(int id, VariableSupport variableSupport) {
+    public void listenToVar(int id, @NonNull VariableSupport variableSupport) {
         add(id, variableSupport);
     }
 
     /**
+     * Is any command listening to this variable
+     *
+     * @param id
+     * @return
+     */
+    public boolean hasListener(int id) {
+        return mVarListeners.get(id) != null;
+    }
+
+    /**
      * List of Commands that need to be updated
      *
      * @param context
      * @return
      */
-    public int getOpsToUpdate(RemoteContext context) {
+    public int getOpsToUpdate(@NonNull RemoteContext context) {
         for (VariableSupport vs : mAllVarListeners) {
             vs.updateVariables(context);
         }
@@ -439,18 +449,18 @@
         updateFloat(RemoteContext.ID_WINDOW_HEIGHT, height);
     }
 
-    public void addCollection(int id, ArrayAccess collection) {
+    public void addCollection(int id, @NonNull ArrayAccess collection) {
         mCollectionMap.put(id & 0xFFFFF, collection);
     }
 
     @Override
     public float getFloatValue(int id, int index) {
-        return mCollectionMap.get(id & 0xFFFFF).getFloatValue(index);
+        return mCollectionMap.get(id & 0xFFFFF).getFloatValue(index); // TODO: potential npe
     }
 
     @Override
-    public float[] getFloats(int id) {
-        return mCollectionMap.get(id & 0xFFFFF).getFloats();
+    public @Nullable float[] getFloats(int id) {
+        return mCollectionMap.get(id & 0xFFFFF).getFloats(); // TODO: potential npe
     }
 
     @Override
@@ -458,11 +468,11 @@
         return mCollectionMap.get(id & 0xFFFFF).getId(index);
     }
 
-    public void putDataMap(int id, DataMap map) {
+    public void putDataMap(int id, @NonNull DataMap map) {
         mDataMapMap.put(id, map);
     }
 
-    public DataMap getDataMap(int id) {
+    public @Nullable DataMap getDataMap(int id) {
         return mDataMapMap.get(id);
     }
 
@@ -471,15 +481,15 @@
         return mCollectionMap.get(id & 0xFFFFF).getLength();
     }
 
-    public void setContext(RemoteContext context) {
+    public void setContext(@NonNull RemoteContext context) {
         mRemoteContext = context;
     }
 
-    public void updateObject(int id, Object value) {
+    public void updateObject(int id, @NonNull Object value) {
         mObjectMap.put(id, value);
     }
 
-    public Object getObject(int id) {
+    public @Nullable Object getObject(int id) {
         return mObjectMap.get(id);
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 23cc5b8..26305bf 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -15,6 +15,7 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
@@ -39,13 +40,15 @@
  * <p>We also contain a PaintContext, so that any operation can draw as needed.
  */
 public abstract class RemoteContext {
-    protected CoreDocument mDocument;
-    public RemoteComposeState mRemoteComposeState;
+    protected @NonNull CoreDocument mDocument =
+            new CoreDocument(); // todo: is this a valid way to initialize? bbade@
+    public @NonNull RemoteComposeState mRemoteComposeState =
+            new RemoteComposeState(); // todo, is this a valid use of RemoteComposeState -- bbade@
     long mStart = System.nanoTime(); // todo This should be set at a hi level
     @Nullable protected PaintContext mPaintContext = null;
     protected float mDensity = 2.75f;
 
-    ContextMode mMode = ContextMode.UNSET;
+    @NonNull ContextMode mMode = ContextMode.UNSET;
 
     int mDebug = 0;
 
@@ -57,7 +60,7 @@
 
     private boolean mAnimate = true;
 
-    public Component lastComponent;
+    public @Nullable Component mLastComponent;
     public long currentTime = 0L;
 
     public float getDensity() {
@@ -65,7 +68,9 @@
     }
 
     public void setDensity(float density) {
-        mDensity = density;
+        if (density > 0) {
+            mDensity = density;
+        }
     }
 
     public boolean isAnimationEnabled() {
@@ -81,7 +86,7 @@
      *
      * @return the CollectionsAccess implementation
      */
-    public CollectionsAccess getCollectionsAccess() {
+    public @Nullable CollectionsAccess getCollectionsAccess() {
         return mRemoteComposeState;
     }
 
@@ -91,7 +96,7 @@
      * @param instanceId
      * @param floatPath
      */
-    public abstract void loadPathData(int instanceId, float[] floatPath);
+    public abstract void loadPathData(int instanceId, @NonNull float[] floatPath);
 
     /**
      * Associate a name with a give id.
@@ -100,7 +105,7 @@
      * @param varId the id (color,integer,float etc.)
      * @param varType thetype
      */
-    public abstract void loadVariableName(String varName, int varId, int varType);
+    public abstract void loadVariableName(@NonNull String varName, int varId, int varType);
 
     /**
      * Save a color under a given id
@@ -135,7 +140,7 @@
      * @param colorName the name of the color to override
      * @param color Override the default color
      */
-    public abstract void setNamedColorOverride(String colorName, int color);
+    public abstract void setNamedColorOverride(@NonNull String colorName, int color);
 
     /**
      * Set the value of a named String. This overrides the string in the document
@@ -143,7 +148,7 @@
      * @param stringName the name of the string to override
      * @param value Override the default string
      */
-    public abstract void setNamedStringOverride(String stringName, String value);
+    public abstract void setNamedStringOverride(@NonNull String stringName, @NonNull String value);
 
     /**
      * Allows to clear a named String.
@@ -152,7 +157,7 @@
      *
      * @param stringName the name of the string to override
      */
-    public abstract void clearNamedStringOverride(String stringName);
+    public abstract void clearNamedStringOverride(@NonNull String stringName);
 
     /**
      * Set the value of a named Integer. This overrides the integer in the document
@@ -160,7 +165,7 @@
      * @param integerName the name of the integer to override
      * @param value Override the default integer
      */
-    public abstract void setNamedIntegerOverride(String integerName, int value);
+    public abstract void setNamedIntegerOverride(@NonNull String integerName, int value);
 
     /**
      * Allows to clear a named Integer.
@@ -169,7 +174,7 @@
      *
      * @param integerName the name of the integer to override
      */
-    public abstract void clearNamedIntegerOverride(String integerName);
+    public abstract void clearNamedIntegerOverride(@NonNull String integerName);
 
     /**
      * Support Collections by registering this collection
@@ -177,20 +182,20 @@
      * @param id id of the collection
      * @param collection the collection under this id
      */
-    public abstract void addCollection(int id, ArrayAccess collection);
+    public abstract void addCollection(int id, @NonNull ArrayAccess collection);
 
-    public abstract void putDataMap(int id, DataMap map);
+    public abstract void putDataMap(int id, @NonNull DataMap map);
 
-    public abstract DataMap getDataMap(int id);
+    public abstract @Nullable DataMap getDataMap(int id);
 
-    public abstract void runAction(int id, String metadata);
+    public abstract void runAction(int id, @NonNull String metadata);
 
     // TODO: we might add an interface to group all valid parameter types
     public abstract void runNamedAction(int textId, Object value);
 
-    public abstract void putObject(int mId, Object command);
+    public abstract void putObject(int mId, @NonNull Object command);
 
-    public abstract Object getObject(int mId);
+    public abstract @Nullable Object getObject(int mId);
 
     public void addTouchListener(TouchListener touchExpression) {}
 
@@ -220,11 +225,11 @@
         this.mTheme = theme;
     }
 
-    public ContextMode getMode() {
+    public @NonNull ContextMode getMode() {
         return mMode;
     }
 
-    public void setMode(ContextMode mode) {
+    public void setMode(@NonNull ContextMode mode) {
         this.mMode = mode;
     }
 
@@ -233,11 +238,11 @@
         return mPaintContext;
     }
 
-    public void setPaintContext(PaintContext paintContext) {
+    public void setPaintContext(@NonNull PaintContext paintContext) {
         this.mPaintContext = paintContext;
     }
 
-    public CoreDocument getDocument() {
+    public @Nullable CoreDocument getDocument() {
         return mDocument;
     }
 
@@ -253,7 +258,7 @@
         this.mDebug = debug;
     }
 
-    public void setDocument(CoreDocument document) {
+    public void setDocument(@NonNull CoreDocument document) {
         this.mDocument = document;
     }
 
@@ -310,11 +315,14 @@
      * Save a bitmap under an imageId
      *
      * @param imageId the id of the image
+     * @param encoding how the data is encoded 0 = png, 1 = raw, 2 = url
+     * @param type the type of the data 0 = RGBA 8888, 1 = 888, 2 = 8 gray
      * @param width the width of the image
      * @param height the height of the image
      * @param bitmap the bytes that represent the image
      */
-    public abstract void loadBitmap(int imageId, int width, int height, byte[] bitmap);
+    public abstract void loadBitmap(
+            int imageId, short encoding, short type, int width, int height, @NonNull byte[] bitmap);
 
     /**
      * Save a string under a given id
@@ -322,7 +330,7 @@
      * @param id the id of the string
      * @param text the value to set
      */
-    public abstract void loadText(int id, String text);
+    public abstract void loadText(int id, @NonNull String text);
 
     /**
      * Get a string given an id
@@ -330,7 +338,7 @@
      * @param id the id of the string
      * @return
      */
-    public abstract String getText(int id);
+    public abstract @Nullable String getText(int id);
 
     /**
      * Load a float
@@ -373,12 +381,12 @@
     public abstract void overrideText(int id, int valueId);
 
     /**
-     * Load an animated float associated with an id Todo: Remove?
+     * Load an animated float associated with an id Todo: Remove? cc @hoford
      *
      * @param id the id of the float
      * @param animatedFloat The animated float
      */
-    public abstract void loadAnimatedFloat(int id, FloatExpression animatedFloat);
+    public abstract void loadAnimatedFloat(int id, @NonNull FloatExpression animatedFloat);
 
     /**
      * Save a shader under and ID
@@ -386,7 +394,7 @@
      * @param id the id of the Shader
      * @param value the shader
      */
-    public abstract void loadShader(int id, ShaderData value);
+    public abstract void loadShader(int id, @NonNull ShaderData value);
 
     /**
      * Get a float given an id
@@ -418,7 +426,7 @@
      * @param id track when this id changes value
      * @param variableSupport call back when value changes
      */
-    public abstract void listensTo(int id, VariableSupport variableSupport);
+    public abstract void listensTo(int id, @NonNull VariableSupport variableSupport);
 
     /**
      * Notify commands with variables have changed
@@ -433,6 +441,7 @@
      * @param id get a shader given the id
      * @return The shader
      */
+    @Nullable
     public abstract ShaderData getShader(int id);
 
     public static final int ID_CONTINUOUS_SEC = 1;
@@ -467,6 +476,10 @@
 
     public static final int ID_LIGHT = 26;
 
+    public static final int ID_DENSITY = 27;
+
+    public static final float FLOAT_DENSITY = Utils.asNan(ID_DENSITY);
+
     /** CONTINUOUS_SEC is seconds from midnight looping every hour 0-3600 */
     public static final float FLOAT_CONTINUOUS_SEC = Utils.asNan(ID_CONTINUOUS_SEC);
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/SerializableToString.java b/core/java/com/android/internal/widget/remotecompose/core/SerializableToString.java
index 8f9741d..79ac789 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/SerializableToString.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/SerializableToString.java
@@ -15,8 +15,10 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
 
 public interface SerializableToString {
-    void serializeToString(int indent, StringSerializer serializer);
+    void serializeToString(int indent, @NonNull StringSerializer serializer);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/VariableSupport.java b/core/java/com/android/internal/widget/remotecompose/core/VariableSupport.java
index 51e58a1..e9fa897 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/VariableSupport.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/VariableSupport.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import android.annotation.NonNull;
+
 /**
  * Interface for operators that interact with variables Through this they register to listen to
  * particular variables and are notified when they change
@@ -26,12 +28,12 @@
      *
      * @param context
      */
-    void registerListening(RemoteContext context);
+    void registerListening(@NonNull RemoteContext context);
 
     /**
      * Called to be notified that the variables you are interested have changed.
      *
      * @param context
      */
-    void updateVariables(RemoteContext context);
+    void updateVariables(@NonNull RemoteContext context);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
index 738e42b..a64b706 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -23,7 +23,7 @@
 public class WireBuffer {
     private static final int BUFFER_SIZE = 1024 * 1024 * 1;
     int mMaxSize;
-    byte[] mBuffer;
+    @NonNull byte[] mBuffer;
     int mIndex = 0;
     int mStartingIndex = 0;
     int mSize = 0;
@@ -44,7 +44,7 @@
         }
     }
 
-    public byte[] getBuffer() {
+    public @NonNull byte[] getBuffer() {
         return mBuffer;
     }
 
@@ -168,14 +168,14 @@
         return java.lang.Double.longBitsToDouble(readLong());
     }
 
-    public byte[] readBuffer() {
+    public @NonNull byte[] readBuffer() {
         int count = readInt();
         byte[] b = Arrays.copyOfRange(mBuffer, mIndex, mIndex + count);
         mIndex += count;
         return b;
     }
 
-    public byte[] readBuffer(int maxSize) {
+    public @NonNull byte[] readBuffer(int maxSize) {
         int count = readInt();
         if (count < 0 || count > maxSize) {
             throw new RuntimeException(
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
index f6dfe2e..0174ce5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
@@ -15,10 +15,14 @@
  */
 package com.android.internal.widget.remotecompose.core.documentation;
 
+import android.annotation.NonNull;
+
 public interface DocumentationBuilder {
-    void add(String value);
+    void add(@NonNull String value);
 
-    DocumentedOperation operation(String category, int id, String name);
+    @NonNull
+    DocumentedOperation operation(@NonNull String category, int id, @NonNull String name);
 
-    DocumentedOperation wipOperation(String category, int id, String name);
+    @NonNull
+    DocumentedOperation wipOperation(@NonNull String category, int id, @NonNull String name);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
index 4b84291..2806a5e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.documentation;
 
+import android.annotation.NonNull;
+
 public interface DocumentedCompanionOperation {
-    void documentation(DocumentationBuilder doc);
+    void documentation(@NonNull DocumentationBuilder doc);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedOperation.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedOperation.java
index 5edecaa..bfab623 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedOperation.java
@@ -16,6 +16,7 @@
 package com.android.internal.widget.remotecompose.core.documentation;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import java.util.ArrayList;
 
@@ -34,13 +35,13 @@
     public static final int FLOAT_ARRAY = 10;
     public static final int INT_ARRAY = 11;
 
-    String mCategory;
+    @NonNull final String mCategory;
     int mId;
-    String mName;
-    String mDescription;
+    @NonNull final String mName;
+    @NonNull String mDescription = "";
 
     boolean mWIP;
-    String mTextExamples;
+    @Nullable String mTextExamples;
 
     @NonNull ArrayList<StringPair> mExamples = new ArrayList<>();
     @NonNull ArrayList<OperationField> mFields = new ArrayList<>();
@@ -77,14 +78,15 @@
         return "UNKNOWN";
     }
 
-    public DocumentedOperation(String category, int id, String name, boolean wip) {
+    public DocumentedOperation(
+            @NonNull String category, int id, @NonNull String name, boolean wip) {
         mCategory = category;
         mId = id;
         mName = name;
         mWIP = wip;
     }
 
-    public DocumentedOperation(String category, int id, String name) {
+    public DocumentedOperation(@NonNull String category, int id, @NonNull String name) {
         this(category, id, name, false);
     }
 
@@ -93,7 +95,7 @@
         return mFields;
     }
 
-    public String getCategory() {
+    public @NonNull String getCategory() {
         return mCategory;
     }
 
@@ -101,6 +103,7 @@
         return mId;
     }
 
+    @NonNull
     public String getName() {
         return mName;
     }
@@ -126,10 +129,12 @@
         return size;
     }
 
+    @Nullable
     public String getDescription() {
         return mDescription;
     }
 
+    @Nullable
     public String getTextExamples() {
         return mTextExamples;
     }
@@ -148,19 +153,20 @@
     }
 
     @NonNull
-    public DocumentedOperation field(int type, String name, String description) {
+    public DocumentedOperation field(int type, @NonNull String name, @NonNull String description) {
         mFields.add(new OperationField(type, name, description));
         return this;
     }
 
     @NonNull
-    public DocumentedOperation field(int type, String name, String varSize, String description) {
+    public DocumentedOperation field(
+            int type, @NonNull String name, @NonNull String varSize, @NonNull String description) {
         mFields.add(new OperationField(type, name, varSize, description));
         return this;
     }
 
     @NonNull
-    public DocumentedOperation possibleValues(String name, int value) {
+    public DocumentedOperation possibleValues(@NonNull String name, int value) {
         if (!mFields.isEmpty()) {
             mFields.get(mFields.size() - 1).possibleValue(name, "" + value);
         }
@@ -168,19 +174,19 @@
     }
 
     @NonNull
-    public DocumentedOperation description(String description) {
+    public DocumentedOperation description(@NonNull String description) {
         mDescription = description;
         return this;
     }
 
     @NonNull
-    public DocumentedOperation examples(String examples) {
+    public DocumentedOperation examples(@NonNull String examples) {
         mTextExamples = examples;
         return this;
     }
 
     @NonNull
-    public DocumentedOperation exampleImage(String name, String imagePath) {
+    public DocumentedOperation exampleImage(@NonNull String name, @NonNull String imagePath) {
         mExamples.add(new StringPair(name, imagePath));
         return this;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
index cbb5ca9..9febcfe 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
@@ -21,20 +21,21 @@
 import java.util.ArrayList;
 
 public class OperationField {
-    int mType;
-    String mName;
-    String mDescription;
+    final int mType;
+    @NonNull final String mName;
+    @NonNull final String mDescription;
     @Nullable String mVarSize = null;
 
     @NonNull ArrayList<StringPair> mPossibleValues = new ArrayList<>();
 
-    public OperationField(int type, String name, String description) {
+    public OperationField(int type, @NonNull String name, @NonNull String description) {
         mType = type;
         mName = name;
         mDescription = description;
     }
 
-    public OperationField(int type, String name, String varSize, String description) {
+    public OperationField(
+            int type, @NonNull String name, @Nullable String varSize, @NonNull String description) {
         mType = type;
         mName = name;
         mDescription = description;
@@ -45,10 +46,12 @@
         return mType;
     }
 
+    @NonNull
     public String getName() {
         return mName;
     }
 
+    @NonNull
     public String getDescription() {
         return mDescription;
     }
@@ -58,7 +61,7 @@
         return mPossibleValues;
     }
 
-    public void possibleValue(String name, String value) {
+    public void possibleValue(@NonNull String name, @NonNull String value) {
         mPossibleValues.add(new StringPair(name, value));
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
index 5b0cedb..c1d8858 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
@@ -15,20 +15,22 @@
  */
 package com.android.internal.widget.remotecompose.core.documentation;
 
-public class StringPair {
-    String mName;
-    String mValue;
+import android.annotation.NonNull;
 
-    StringPair(String name, String value) {
+public class StringPair {
+    final @NonNull String mName;
+    final @NonNull String mValue;
+
+    StringPair(@NonNull String name, @NonNull String value) {
         mName = name;
         mValue = value;
     }
 
-    public String getName() {
+    public @NonNull String getName() {
         return mName;
     }
 
-    public String getValue() {
+    public @NonNull String getValue() {
         return mValue;
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
index 8da0e18..9480076 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
@@ -17,6 +17,7 @@
 
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT_ARRAY;
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.SHORT;
 
 import android.annotation.NonNull;
 
@@ -41,10 +42,19 @@
     int mImageId;
     int mImageWidth;
     int mImageHeight;
-    byte[] mBitmap;
+    short mType;
+    short mEncoding;
+    @NonNull final byte[] mBitmap;
     public static final int MAX_IMAGE_DIMENSION = 8000;
+    public static final short ENCODING_INLINE = 0;
+    public static final short ENCODING_URL = 1;
+    public static final short ENCODING_FILE = 2;
+    public static final short TYPE_PNG_8888 = 0;
+    public static final short TYPE_PNG = 1;
+    public static final short TYPE_RAW8 = 2;
+    public static final short TYPE_RAW8888 = 3;
 
-    public BitmapData(int imageId, int width, int height, byte[] bitmap) {
+    public BitmapData(int imageId, int width, int height, @NonNull byte[] bitmap) {
         this.mImageId = imageId;
         this.mImageWidth = width;
         this.mImageHeight = height;
@@ -92,6 +102,23 @@
         buffer.writeBuffer(bitmap);
     }
 
+    public static void apply(
+            @NonNull WireBuffer buffer,
+            int imageId,
+            short type,
+            short width,
+            short encoding,
+            short height,
+            @NonNull byte[] bitmap) {
+        buffer.start(OP_CODE);
+        buffer.writeInt(imageId);
+        int w = (((int) type) << 16) | width;
+        int h = (((int) encoding) << 16) | height;
+        buffer.writeInt(w);
+        buffer.writeInt(h);
+        buffer.writeBuffer(bitmap);
+    }
+
     public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         int imageId = buffer.readInt();
         int width = buffer.readInt();
@@ -110,19 +137,22 @@
         doc.operation("Data Operations", OP_CODE, CLASS_NAME)
                 .description("Bitmap data")
                 .field(DocumentedOperation.INT, "id", "id of bitmap data")
+                .field(SHORT, "type", "width of the image")
+                .field(SHORT, "width", "width of the image")
+                .field(SHORT, "encoding", "height of the image")
                 .field(INT, "width", "width of the image")
-                .field(INT, "height", "height of the image")
+                .field(SHORT, "height", "height of the image")
                 .field(INT_ARRAY, "values", "length", "Array of ints");
     }
 
     @Override
     public void apply(@NonNull RemoteContext context) {
-        context.loadBitmap(mImageId, mImageWidth, mImageHeight, mBitmap);
+        context.loadBitmap(mImageId, mEncoding, mType, mImageWidth, mImageHeight, mBitmap);
     }
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
index 83d0ac7..310b194 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
@@ -109,7 +109,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorConstant.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorConstant.java
index 929c9a60..34e93f5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorConstant.java
@@ -93,7 +93,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java
index 3d840c5..c947d11 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java
@@ -255,7 +255,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ComponentValue.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ComponentValue.java
index 142c97b2..b0ccd187 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/ComponentValue.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ComponentValue.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -75,7 +74,7 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // Nothing
     }
 
@@ -123,9 +122,9 @@
         buffer.writeInt(valueId);
     }
 
-    @Nullable
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return null;
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java
index ba02b91..bfaf139 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java
@@ -35,17 +35,17 @@
 public class DataListFloat implements VariableSupport, ArrayAccess, Operation {
     private static final int OP_CODE = Operations.FLOAT_LIST;
     private static final String CLASS_NAME = "IdListData";
-    int mId;
-    float[] mValues;
+    private final int mId;
+    @NonNull private final float[] mValues;
     private static final int MAX_FLOAT_ARRAY = 2000;
 
-    public DataListFloat(int id, float[] values) {
+    public DataListFloat(int id, @NonNull float[] values) {
         mId = id;
         mValues = values;
     }
 
     @Override
-    public void updateVariables(RemoteContext context) {
+    public void updateVariables(@NonNull RemoteContext context) {
         // TODO add support for variables in arrays
     }
 
@@ -103,7 +103,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
@@ -117,6 +117,7 @@
         return mValues[index];
     }
 
+    @NonNull
     @Override
     public float[] getFloats() {
         return mValues;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java
index b9820f8..9b286b9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT_ARRAY;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -35,20 +36,20 @@
 public class DataListIds implements VariableSupport, ArrayAccess, Operation {
     private static final int OP_CODE = Operations.ID_LIST;
     private static final String CLASS_NAME = "IdListData";
-    int mId;
-    int[] mIds;
+    private final int mId;
+    @NonNull private final int[] mIds;
     private static final int MAX_LIST = 2000;
 
-    public DataListIds(int id, int[] ids) {
+    public DataListIds(int id, @NonNull int[] ids) {
         mId = id;
         mIds = ids;
     }
 
     @Override
-    public void updateVariables(RemoteContext context) {}
+    public void updateVariables(@NonNull RemoteContext context) {}
 
     @Override
-    public void registerListening(RemoteContext context) {}
+    public void registerListening(@NonNull RemoteContext context) {}
 
     @Override
     public void write(@NonNull WireBuffer buffer) {
@@ -94,7 +95,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
@@ -113,6 +114,7 @@
         return mIds[index];
     }
 
+    @Nullable
     @Override
     public float[] getFloats() {
         return null;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapIds.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapIds.java
index fb559bb..643afc8 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapIds.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapIds.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.UTF8;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -34,7 +35,7 @@
     private static final int OP_CODE = Operations.ID_MAP;
     private static final String CLASS_NAME = "DataMapIds";
     int mId;
-    DataMap mDataMap;
+    final DataMap mDataMap;
 
     private static final int MAX_MAP = 2000;
 
@@ -44,6 +45,7 @@
     public static final byte TYPE_LONG = 3;
     public static final byte TYPE_BOOLEAN = 4;
 
+    @NonNull
     private String typeString(byte type) {
         switch (type) {
             case TYPE_STRING:
@@ -60,7 +62,7 @@
         return "?";
     }
 
-    public DataMapIds(int id, String[] names, byte[] types, int[] ids) {
+    public DataMapIds(int id, @NonNull String[] names, @NonNull byte[] types, @NonNull int[] ids) {
         mId = id;
         mDataMap = new DataMap(names, types, ids);
     }
@@ -88,7 +90,11 @@
     }
 
     public static void apply(
-            @NonNull WireBuffer buffer, int id, @NonNull String[] names, byte[] type, int[] ids) {
+            @NonNull WireBuffer buffer,
+            int id,
+            @NonNull String[] names,
+            @Nullable byte[] type, // todo: can we make this not nullable?
+            @NonNull int[] ids) {
         buffer.start(OP_CODE);
         buffer.writeInt(id);
         buffer.writeInt(names.length);
@@ -128,7 +134,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapLookup.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapLookup.java
index fb5e5d1..eae532c 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapLookup.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataMapLookup.java
@@ -17,6 +17,8 @@
 
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
@@ -50,10 +52,11 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         apply(buffer, mId, mDataMapId, mStringId);
     }
 
+    @NonNull
     @Override
     public String toString() {
         return "DataMapLookup[" + mId + "] = " + Utils.idString(mDataMapId) + " " + mStringId;
@@ -64,6 +67,7 @@
      *
      * @return the name of the class
      */
+    @NonNull
     public static String name() {
         return CLASS_NAME;
     }
@@ -85,7 +89,7 @@
      * @param dataMapId the map to extract from
      * @param keyStringId the map to extract from
      */
-    public static void apply(WireBuffer buffer, int id, int dataMapId, int keyStringId) {
+    public static void apply(@NonNull WireBuffer buffer, int id, int dataMapId, int keyStringId) {
         buffer.start(OP_CODE);
         buffer.writeInt(id);
         buffer.writeInt(dataMapId);
@@ -98,14 +102,14 @@
      * @param buffer buffer
      * @param operations the created command is added to the list
      */
-    public static void read(WireBuffer buffer, List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         int id = buffer.readInt();
         int mapId = buffer.readInt();
         int stringId = buffer.readInt();
         operations.add(new DataMapLookup(id, mapId, stringId));
     }
 
-    public static void documentation(DocumentationBuilder doc) {
+    public static void documentation(@NonNull DocumentationBuilder doc) {
         doc.operation("Expressions Operations", OP_CODE, CLASS_NAME)
                 .description("A float and its associated id")
                 .field(INT, "id", "id of float")
@@ -114,7 +118,7 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         String str = context.getText(mStringId);
         DataMap data = context.getDataMap(mDataMapId);
         int pos = data.getPos(str);
@@ -141,8 +145,9 @@
         }
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java
index 984599e..c1e2e66 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java
@@ -60,11 +60,11 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         write(buffer, mV1, mV2);
     }
 
-    protected abstract void write(WireBuffer buffer, float v1, float v2);
+    protected abstract void write(@NonNull WireBuffer buffer, float v1, float v2);
 
     protected interface Maker {
         DrawBase2 create(float v1, float v2);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
index 825da52..6fedea3 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
@@ -70,11 +70,11 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         write(buffer, mV1, mV2, mV3);
     }
 
-    protected abstract void write(WireBuffer buffer, float v1, float v2, float v3);
+    protected abstract void write(@NonNull WireBuffer buffer, float v1, float v2, float v3);
 
     interface Maker {
         DrawBase3 create(float v1, float v2, float v3);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
index a23bcb9..aa9cc68 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
@@ -77,11 +77,12 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         write(buffer, mX1, mY1, mX2, mY2);
     }
 
-    protected abstract void write(WireBuffer buffer, float v1, float v2, float v3, float v4);
+    protected abstract void write(
+            @NonNull WireBuffer buffer, float v1, float v2, float v3, float v4);
 
     protected interface Maker {
         DrawBase4 create(float v1, float v2, float v3, float v4);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java
index 68c9f8c..6c288a35 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java
@@ -91,12 +91,12 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         write(buffer, mV1, mV2, mV3, mV4, mV5, mV6);
     }
 
     protected abstract void write(
-            WireBuffer buffer, float v1, float v2, float v3, float v4, float v5, float v6);
+            @NonNull WireBuffer buffer, float v1, float v2, float v3, float v4, float v5, float v6);
 
     @NonNull
     @Override
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapScaled.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapScaled.java
index e20bcd2..e9f81d5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapScaled.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapScaled.java
@@ -46,6 +46,7 @@
     int mContentDescId;
     float mScaleFactor, mOutScaleFactor;
     int mScaleType;
+    int mMode;
 
     @NonNull ImageScaling mScaling = new ImageScaling();
     public static final int SCALE_NONE = ImageScaling.SCALE_NONE;
@@ -79,7 +80,8 @@
         mOutDstTop = mDstTop = dstTop;
         mOutDstRight = mDstRight = dstRight;
         mOutDstBottom = mDstBottom = dstBottom;
-        mScaleType = type;
+        mScaleType = type & 0xFF;
+        mMode = type >> 8;
         mOutScaleFactor = mScaleFactor = scale;
         this.mContentDescId = cdId;
     }
@@ -308,8 +310,14 @@
                 mOutScaleFactor);
         context.save();
         context.clipRect(mOutDstLeft, mOutDstTop, mOutDstRight, mOutDstBottom);
+
+        int imageId = mImageId;
+        if ((mMode & 0x1) != 0) {
+            imageId = context.getContext().getInteger(imageId);
+        }
+
         context.drawBitmap(
-                mImageId,
+                imageId,
                 (int) mOutSrcLeft,
                 (int) mOutSrcTop,
                 (int) mOutSrcRight,
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
index 89390ac..17aaf3b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
@@ -94,7 +94,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java
index e1c6c25..eef9746 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java
@@ -33,6 +33,7 @@
 import com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression;
 import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap;
 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.SpringStopEngine;
 
 import java.util.List;
 
@@ -45,21 +46,26 @@
     private static final int OP_CODE = Operations.ANIMATED_FLOAT;
     private static final String CLASS_NAME = "FloatExpression";
     public int mId;
-    public float[] mSrcValue;
-    public float[] mSrcAnimation;
-    public FloatAnimation mFloatAnimation;
-    public float[] mPreCalcValue;
+    @NonNull public float[] mSrcValue;
+    @Nullable public float[] mSrcAnimation;
+    @Nullable public FloatAnimation mFloatAnimation;
+    @Nullable private SpringStopEngine mSpring;
+    @Nullable public float[] mPreCalcValue;
     private float mLastChange = Float.NaN;
     private float mLastCalculatedValue = Float.NaN;
     @NonNull AnimatedFloatExpression mExp = new AnimatedFloatExpression();
     public static final int MAX_EXPRESSION_SIZE = 32;
 
-    public FloatExpression(int id, float[] value, float[] animation) {
+    public FloatExpression(int id, @NonNull float[] value, @Nullable float[] animation) {
         this.mId = id;
         this.mSrcValue = value;
         this.mSrcAnimation = animation;
         if (mSrcAnimation != null) {
-            mFloatAnimation = new FloatAnimation(mSrcAnimation);
+            if (mSrcAnimation.length > 4 && mSrcAnimation[0] == 0) {
+                mSpring = new SpringStopEngine(mSrcAnimation);
+            } else {
+                mFloatAnimation = new FloatAnimation(mSrcAnimation);
+            }
         }
     }
 
@@ -75,12 +81,23 @@
             if (Float.isNaN(v)
                     && !AnimatedFloatExpression.isMathOperator(v)
                     && !NanMap.isDataVariable(v)) {
+                int id = Utils.idFromNan(v);
                 float newValue = context.getFloat(Utils.idFromNan(v));
+
+                // TODO: rethink the lifecycle for variable updates
+                if (id == RemoteContext.ID_DENSITY && newValue == 0f) {
+                    newValue = 1f;
+                }
                 if (mFloatAnimation != null) {
                     if (mPreCalcValue[i] != newValue) {
                         value_changed = true;
                         mPreCalcValue[i] = newValue;
                     }
+                } else if (mSpring != null) {
+                    if (mPreCalcValue[i] != newValue) {
+                        value_changed = true;
+                        mPreCalcValue[i] = newValue;
+                    }
                 } else {
                     mPreCalcValue[i] = newValue;
                 }
@@ -106,6 +123,8 @@
                 mFloatAnimation.setInitialValue(mFloatAnimation.getTargetValue());
             }
             mFloatAnimation.setTargetValue(v);
+        } else if (value_changed && mSpring != null) {
+            mSpring.setTargetValue(v);
         }
     }
 
@@ -130,6 +149,9 @@
         if (mFloatAnimation != null) {
             float f = mFloatAnimation.get(t - mLastChange);
             context.loadFloat(mId, f);
+        } else if (mSpring != null) {
+            float f = mSpring.get(t - mLastChange);
+            context.loadFloat(mId, f);
         } else {
             context.loadFloat(
                     mId,
@@ -137,6 +159,21 @@
         }
     }
 
+    /**
+     * Evaluate the expression
+     *
+     * @param context current context
+     * @return the resulting value
+     */
+    public float evaluate(@NonNull RemoteContext context) {
+        updateVariables(context);
+        float t = context.getAnimationTime();
+        if (Float.isNaN(mLastChange)) {
+            mLastChange = t;
+        }
+        return mExp.eval(context.getCollectionsAccess(), mPreCalcValue, mPreCalcValue.length);
+    }
+
     @Override
     public void write(@NonNull WireBuffer buffer) {
         apply(buffer, mId, mSrcValue, mSrcAnimation);
@@ -256,7 +293,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
index 1979bc5..009aa03 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -111,7 +111,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java
index 6375f00..c49f74d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT_ARRAY;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -43,13 +44,13 @@
     public int mId;
     private int mMask;
     private int mPreMask;
-    public int[] mSrcValue;
-    public int[] mPreCalcValue;
+    @NonNull public final int[] mSrcValue;
+    @Nullable public int[] mPreCalcValue;
     private float mLastChange = Float.NaN;
     public static final int MAX_SIZE = 320;
     @NonNull IntegerExpressionEvaluator mExp = new IntegerExpressionEvaluator();
 
-    public IntegerExpression(int id, int mask, int[] value) {
+    public IntegerExpression(int id, int mask, @NonNull int[] value) {
         this.mId = id;
         this.mMask = mask;
         this.mSrcValue = value;
@@ -188,7 +189,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
index 6a620e5..4e7ee4d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
@@ -37,7 +37,7 @@
         apply(buffer);
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         MatrixRestore op = new MatrixRestore();
         operations.add(op);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
index 1880b19..09f54a5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
@@ -41,7 +41,7 @@
         return "MatrixSave;";
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         MatrixSave op = new MatrixSave();
         operations.add(op);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java b/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java
index 6310521e..5ca91af 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java
@@ -33,16 +33,16 @@
 public class NamedVariable implements Operation {
     private static final int OP_CODE = Operations.NAMED_VARIABLE;
     private static final String CLASS_NAME = "NamedVariable";
-    public int mVarId;
-    public String mVarName;
-    public int mVarType;
+    public final int mVarId;
+    public final @NonNull String mVarName;
+    public final int mVarType;
     public static final int MAX_STRING_SIZE = 4000;
     public static final int COLOR_TYPE = 2;
     public static final int FLOAT_TYPE = 1;
     public static final int STRING_TYPE = 0;
     public static final int IMAGE_TYPE = 3;
 
-    public NamedVariable(int varId, int varType, String name) {
+    public NamedVariable(int varId, int varType, @NonNull String name) {
         this.mVarId = varId;
         this.mVarType = varType;
         this.mVarName = name;
@@ -111,7 +111,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
index 527d5610..b24df8a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
@@ -90,7 +90,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
index 06a1fec..509f362 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
@@ -73,7 +73,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return pathString(mFloatPath);
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
index 6ff9ad7..8494126 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
@@ -196,7 +196,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
index c2d62a7..109945f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
@@ -60,7 +60,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java
index ae61c3a..e967ff4 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java
@@ -48,7 +48,7 @@
     int mShaderID; // allows shaders to be referenced by number
     @Nullable HashMap<String, float[]> mUniformRawFloatMap = null;
     @Nullable HashMap<String, float[]> mUniformFloatMap = null;
-    @Nullable HashMap<String, int[]> mUniformIntMap = null;
+    @Nullable HashMap<String, int[]> mUniformIntMap;
     @Nullable HashMap<String, Integer> mUniformBitmapMap = null;
 
     public ShaderData(
@@ -104,8 +104,8 @@
      * @param name name of uniform
      * @return value of uniform
      */
-    public float[] getUniformFloats(String name) {
-        return mUniformFloatMap.get(name);
+    public @NonNull float[] getUniformFloats(@NonNull String name) {
+        return mUniformFloatMap != null ? mUniformFloatMap.get(name) : new float[0];
     }
 
     /**
@@ -125,8 +125,8 @@
      * @param name Name of uniform
      * @return value of uniform
      */
-    public int[] getUniformInts(String name) {
-        return mUniformIntMap.get(name);
+    public @NonNull int[] getUniformInts(@NonNull String name) {
+        return mUniformIntMap != null ? mUniformIntMap.get(name) : new int[0];
     }
 
     /**
@@ -146,8 +146,10 @@
      * @param name Name of bitmap uniform
      * @return Bitmap ID
      */
-    public int getUniformBitmapId(String name) {
-        return mUniformBitmapMap.get(name);
+    public int getUniformBitmapId(@NonNull String name) {
+        return mUniformBitmapMap != null
+                ? mUniformBitmapMap.get(name)
+                : -1; // TODO: what is the proper return value here? -- bbade@
     }
 
     @Override
@@ -169,7 +171,7 @@
 
     @Override
     public void updateVariables(@NonNull RemoteContext context) {
-        for (String name : mUniformRawFloatMap.keySet()) {
+        for (String name : mUniformRawFloatMap.keySet()) { // TODO: potential npe
             float[] value = mUniformRawFloatMap.get(name);
             float[] out = null;
             for (int i = 0; i < value.length; i++) {
@@ -186,7 +188,7 @@
 
     @Override
     public void registerListening(@NonNull RemoteContext context) {
-        for (String name : mUniformRawFloatMap.keySet()) {
+        for (String name : mUniformRawFloatMap.keySet()) { // TODO: potential npe
             float[] value = mUniformRawFloatMap.get(name);
             for (float v : value) {
                 if (Float.isNaN(v)) {
@@ -340,7 +342,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
index dbaef7e..ade008e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
@@ -34,11 +34,11 @@
 public class TextData implements Operation, SerializableToString {
     private static final int OP_CODE = Operations.DATA_TEXT;
     private static final String CLASS_NAME = "TextData";
-    public int mTextId;
-    public String mText;
+    public final int mTextId;
+    @NonNull public final String mText;
     public static final int MAX_STRING_SIZE = 4000;
 
-    public TextData(int textId, String text) {
+    public TextData(int textId, @NonNull String text) {
         this.mTextId = textId;
         this.mText = text;
     }
@@ -90,7 +90,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java
index fb5087f..865ee81f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java
@@ -185,7 +185,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLength.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLength.java
index e148fb9..6ff687b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLength.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLength.java
@@ -17,6 +17,8 @@
 
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
@@ -38,16 +40,17 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         apply(buffer, mLengthId, mTextId);
     }
 
+    @NonNull
     @Override
     public String toString() {
         return CLASS_NAME + "[" + mLengthId + "] = " + mTextId;
     }
 
-    public static String name() {
+    public static @NonNull String name() {
         return CLASS_NAME;
     }
 
@@ -62,19 +65,19 @@
      * @param lengthId the id to output
      * @param textId the id of the text to measure
      */
-    public static void apply(WireBuffer buffer, int lengthId, int textId) {
+    public static void apply(@NonNull WireBuffer buffer, int lengthId, int textId) {
         buffer.start(OP_CODE);
         buffer.writeInt(lengthId);
         buffer.writeInt(textId);
     }
 
-    public static void read(WireBuffer buffer, List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         int lengthId = buffer.readInt();
         int textId = buffer.readInt();
         operations.add(new TextLength(lengthId, textId));
     }
 
-    public static void documentation(DocumentationBuilder doc) {
+    public static void documentation(@NonNull DocumentationBuilder doc) {
         doc.operation("Expressions Operations", OP_CODE, CLASS_NAME)
                 .description("get the length of the text and store in float table")
                 .field(INT, "id", "id of float length")
@@ -82,12 +85,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         context.loadFloat(mLengthId, context.getText(mTextId).length());
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java
index 2129edd..cc812a8 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java
@@ -126,7 +126,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java
index ea550cb..74be698 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java
@@ -119,7 +119,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMeasure.java
index 0281d69..6d48f67 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMeasure.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMeasure.java
@@ -19,6 +19,8 @@
 import static com.android.internal.widget.remotecompose.core.PaintContext.TEXT_MEASURE_MONOSPACE_WIDTH;
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.PaintContext;
@@ -55,16 +57,16 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         apply(buffer, mId, mTextId, mType);
     }
 
     @Override
-    public String toString() {
+    public @NonNull String toString() {
         return "FloatConstant[" + mId + "] = " + mTextId + " " + mType;
     }
 
-    public static String name() {
+    public static @NonNull String name() {
         return CLASS_NAME;
     }
 
@@ -80,21 +82,21 @@
      * @param textId the id
      * @param type the value of the float
      */
-    public static void apply(WireBuffer buffer, int id, int textId, int type) {
+    public static void apply(@NonNull WireBuffer buffer, int id, int textId, int type) {
         buffer.start(OP_CODE);
         buffer.writeInt(id);
         buffer.writeInt(textId);
         buffer.writeInt(type);
     }
 
-    public static void read(WireBuffer buffer, List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         int id = buffer.readInt();
         int textId = buffer.readInt();
         int type = buffer.readInt();
         operations.add(new TextMeasure(id, textId, type));
     }
 
-    public static void documentation(DocumentationBuilder doc) {
+    public static void documentation(@NonNull DocumentationBuilder doc) {
         doc.operation("Expressions Operations", OP_CODE, CLASS_NAME)
                 .description("A float and its associated id")
                 .field(INT, "id", "id of float result of the measure")
@@ -102,15 +104,16 @@
                 .field(INT, "type", "type: measure 0=width,1=height");
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
-    float[] mBounds = new float[4];
+    @NonNull float[] mBounds = new float[4];
 
     @Override
-    public void paint(PaintContext context) {
+    public void paint(@NonNull PaintContext context) {
         int val = mType & 255;
         int flags = mType >> 8;
         context.getTextBounds(mTextId, 0, -1, flags, mBounds);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java
index fa18b4d..ecd5baa 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java
@@ -102,7 +102,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
index 1e90ab1..d265070 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
@@ -68,7 +68,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
index b25a7f6..1bb7b2a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
@@ -20,6 +20,8 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.SHORT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
@@ -71,6 +73,7 @@
     boolean mWrapMode = false;
     float[] mNotches;
     float[] mStopSpec;
+    float[] mOutStopSpec;
     int mTouchEffects;
     float mVelocityId;
 
@@ -98,6 +101,9 @@
         mOutDefValue = mDefValue = defValue;
         mMode = STOP_ABSOLUTE_POS == stopMode ? 1 : 0;
         mOutMax = mMax = max;
+        if (stopSpec != null) {
+            mOutStopSpec = Arrays.copyOf(stopSpec, stopSpec.length);
+        }
         mTouchEffects = touchEffects;
         mVelocityId = velocityId;
         if (Float.isNaN(min) && Utils.idFromNan(min) == 0) {
@@ -108,10 +114,8 @@
         mStopMode = stopMode;
         mStopSpec = stopSpec;
         if (easingSpec != null) {
-            Utils.log("easingSpec  " + Arrays.toString(easingSpec));
             if (easingSpec.length >= 4) {
                 if (Float.floatToRawIntBits(easingSpec[0]) == 0) {
-                    Utils.log("easingSpec[2]  " + easingSpec[2]);
                     mMaxTime = easingSpec[1];
                     mMaxAcceleration = easingSpec[2];
                     mMaxVelocity = easingSpec[3];
@@ -126,6 +130,9 @@
         if (mPreCalcValue == null || mPreCalcValue.length != mSrcExp.length) {
             mPreCalcValue = new float[mSrcExp.length];
         }
+        if (mOutStopSpec == null || mOutStopSpec.length != mStopSpec.length) {
+            mOutStopSpec = new float[mStopSpec.length];
+        }
         if (Float.isNaN(mMax)) {
             mOutMax = context.getFloat(Utils.idFromNan(mMax));
         }
@@ -150,6 +157,15 @@
                 mPreCalcValue[i] = mSrcExp[i];
             }
         }
+        for (int i = 0; i < mStopSpec.length; i++) {
+            float v = mStopSpec[i];
+            if (Float.isNaN(v)) {
+                float newValue = context.getFloat(Utils.idFromNan(v));
+                mOutStopSpec[i] = newValue;
+            } else {
+                mOutStopSpec[i] = v;
+            }
+        }
         float v = mLastCalculatedValue;
         if (value_changed) { // inputs changed check if output changed
             v = mExp.eval(mPreCalcValue, mPreCalcValue.length);
@@ -181,6 +197,11 @@
                 context.listensTo(Utils.idFromNan(v), this);
             }
         }
+        for (float v : mStopSpec) {
+            if (Float.isNaN(v)) {
+                context.listensTo(Utils.idFromNan(v), this);
+            }
+        }
     }
 
     private float wrap(float pos) {
@@ -211,12 +232,14 @@
             case STOP_INSTANTLY:
                 return pos;
             case STOP_NOTCHES_EVEN:
-                int evenSpacing = (int) mStopSpec[0];
-                float step = (mOutMax - min) / evenSpacing;
+                int evenSpacing = (int) mOutStopSpec[0];
+                float notchMax = (mOutStopSpec.length > 1) ? mOutStopSpec[1] : mOutMax;
+                float step = (notchMax - min) / evenSpacing;
 
                 float notch = min + step * (int) (0.5f + (target - mOutMin) / step);
-
-                notch = Math.max(Math.min(notch, mOutMax), min);
+                if (!mWrapMode) {
+                    notch = Math.max(Math.min(notch, mOutMax), min);
+                }
                 return notch;
             case STOP_NOTCHES_PERCENTS:
                 positions = new float[mStopSpec.length];
@@ -265,7 +288,6 @@
         float next = mCurrentValue;
         mLastValue = next;
 
-        //        System.out.println(mStopMode + "    " + prev + "  -> " + next);
         float min = (mWrapMode) ? 0 : mOutMin;
         float max = mOutMax;
 
@@ -309,7 +331,7 @@
 
     @Override
     public void apply(RemoteContext context) {
-        Component comp = context.lastComponent;
+        Component comp = context.mLastComponent;
         if (comp != null) {
             float x = comp.getX();
             float y = comp.getY();
@@ -329,7 +351,6 @@
         updateVariables(context);
         if (mUnmodified) {
             mCurrentValue = mOutDefValue;
-
             context.loadFloat(mId, wrap(mCurrentValue));
             return;
         }
@@ -337,7 +358,11 @@
             float time = context.getAnimationTime() - mTouchUpTime;
             float value = mEasyTouch.getPos(time);
             mCurrentValue = value;
-            value = wrap(value);
+            if (mWrapMode) {
+                value = wrap(value);
+            } else {
+                value = Math.min(Math.max(value, mOutMin), mOutMax);
+            }
             context.loadFloat(mId, value);
             if (mEasyTouch.getDuration() < time) {
                 mEasingToStop = false;
@@ -410,7 +435,8 @@
         mTouchUpTime = context.getAnimationTime();
 
         float dest = getStopPosition(value, slope);
-        mEasyTouch.config(value, dest, slope, mMaxTime, mMaxAcceleration, mMaxVelocity, null);
+        float time = mMaxTime * Math.abs(dest - value) / (2 * mMaxVelocity);
+        mEasyTouch.config(value, dest, slope, time, mMaxAcceleration, mMaxVelocity, null);
         mEasingToStop = true;
     }
 
@@ -543,7 +569,6 @@
         int stopLen = stopLogic & 0xFFFF;
         int stopMode = stopLogic >> 16;
 
-        Utils.log("stopMode " + stopMode + " stopLen " + stopLen);
         float[] stopsData = new float[stopLen];
         for (int i = 0; i < stopsData.length; i++) {
             stopsData[i] = buffer.readFloat();
@@ -592,8 +617,9 @@
                 .field(FLOAT, "wrapValue", "> [Wrap value] ");
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return indent + toString();
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
index 03f7e05..baca3e0 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
@@ -69,7 +69,7 @@
      * @param value
      * @return
      */
-    public static String floatToString(float idvalue, float value) {
+    public static @NonNull String floatToString(float idvalue, float value) {
         if (Float.isNaN(idvalue)) {
             if (idFromNan(value) == 0) {
                 return "NaN";
@@ -85,7 +85,7 @@
      * @param value
      * @return
      */
-    public static String floatToString(float value) {
+    public static @NonNull String floatToString(float value) {
         if (Float.isNaN(value)) {
             if (idFromNan(value) == 0) {
                 return "NaN";
@@ -100,7 +100,7 @@
      *
      * @param str
      */
-    public static void log(String str) {
+    public static void log(@NonNull String str) {
         StackTraceElement s = new Throwable().getStackTrace()[1];
         System.out.println(
                 "("
@@ -119,7 +119,7 @@
      * @param str
      * @param n
      */
-    public static void logStack(String str, int n) {
+    public static void logStack(@NonNull String str, int n) {
         StackTraceElement[] st = new Throwable().getStackTrace();
         for (int i = 1; i < n + 1; i++) {
             StackTraceElement s = st[i];
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ActionOperation.java
index bdc2a886..7f1d101 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ActionOperation.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
@@ -22,8 +24,12 @@
 
 /** Operations representing actions on the document */
 public interface ActionOperation extends Operation {
-    void serializeToString(int indent, StringSerializer serializer);
+    void serializeToString(int indent, @NonNull StringSerializer serializer);
 
     void runAction(
-            RemoteContext context, CoreDocument document, Component component, float x, float y);
+            @NonNull RemoteContext context,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
+            float x,
+            float y);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/AnimatableValue.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/AnimatableValue.java
index e789710..19f4c2b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/AnimatableValue.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/AnimatableValue.java
@@ -77,4 +77,9 @@
 
         return mValue;
     }
+
+    @Override
+    public String toString() {
+        return "AnimatableValue{mId=" + mId + "}";
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasContent.java
index 9886518..aa8f7580 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasContent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasContent.java
@@ -18,6 +18,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -35,7 +36,7 @@
             float y,
             float width,
             float height,
-            Component parent,
+            @Nullable Component parent,
             int animationId) {
         super(parent, componentId, animationId, x, y, width, height);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ClickModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ClickModifierOperation.java
index b567538..f44e20d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ClickModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ClickModifierOperation.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -80,7 +79,7 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         for (Operation op : mList) {
             if (op instanceof TextData) {
                 op.apply(context);
@@ -90,7 +89,7 @@
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
@@ -137,7 +136,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         mWidth = width;
         mHeight = height;
     }
@@ -154,8 +153,8 @@
 
     @Override
     public void onClick(
-            RemoteContext context,
-            CoreDocument document,
+            @NonNull RemoteContext context,
+            @NonNull CoreDocument document,
             @NonNull Component component,
             float x,
             float y) {
@@ -171,6 +170,7 @@
                 ((ActionOperation) o).runAction(context, document, component, x, y);
             }
         }
+        context.hapticEffect(3);
     }
 
     @NonNull
@@ -182,7 +182,7 @@
         buffer.start(OP_CODE);
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         operations.add(new ClickModifierOperation());
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
index f4f4ee2..fbfc796 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
@@ -50,12 +50,12 @@
     protected float mY;
     protected float mWidth;
     protected float mHeight;
-    protected Component mParent;
+    @Nullable protected Component mParent;
     protected int mAnimationId = -1;
-    public Visibility mVisibility = Visibility.VISIBLE;
-    public Visibility mScheduledVisibility = Visibility.VISIBLE;
+    @NonNull public Visibility mVisibility = Visibility.VISIBLE;
+    @NonNull public Visibility mScheduledVisibility = Visibility.VISIBLE;
     @NonNull public ArrayList<Operation> mList = new ArrayList<>();
-    public PaintOperation mPreTranslate;
+    public PaintOperation mPreTranslate; // todo, can we initialize this here and make it NonNull?
     public boolean mNeedsMeasure = true;
     public boolean mNeedsRepaint = false;
     @Nullable public AnimateMeasure mAnimateMeasure;
@@ -99,6 +99,7 @@
         return mAnimationId;
     }
 
+    @Nullable
     public Component getParent() {
         return mParent;
     }
@@ -160,7 +161,7 @@
     }
 
     public Component(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -177,7 +178,12 @@
     }
 
     public Component(
-            int componentId, float x, float y, float width, float height, Component parent) {
+            int componentId,
+            float x,
+            float y,
+            float width,
+            float height,
+            @Nullable Component parent) {
         this(parent, componentId, -1, x, y, width, height);
     }
 
@@ -211,7 +217,7 @@
         return mNeedsMeasure;
     }
 
-    public void setParent(Component parent) {
+    public void setParent(@Nullable Component parent) {
         mParent = parent;
     }
 
@@ -222,8 +228,8 @@
      * @param context the current context
      */
     public void updateVariables(@NonNull RemoteContext context) {
-        Component prev = context.lastComponent;
-        context.lastComponent = this;
+        Component prev = context.mLastComponent;
+        context.mLastComponent = this;
 
         if (!mComponentValues.isEmpty()) {
             updateComponentValues(context);
@@ -236,13 +242,21 @@
                 o.apply(context);
             }
         }
-        context.lastComponent = prev;
+        context.mLastComponent = prev;
     }
 
-    public void addComponentValue(ComponentValue v) {
+    public void addComponentValue(@NonNull ComponentValue v) {
         mComponentValues.add(v);
     }
 
+    public float intrinsicWidth() {
+        return getWidth();
+    }
+
+    public float intrinsicHeight() {
+        return getHeight();
+    }
+
     public enum Visibility {
         GONE,
         VISIBLE,
@@ -253,13 +267,13 @@
         if (mVisibility != Visibility.VISIBLE || mParent == null) {
             return mVisibility == Visibility.VISIBLE;
         }
-        if (mParent != null) {
+        if (mParent != null) { // TODO: this is always true -- bbade@
             return mParent.isVisible();
         }
         return true;
     }
 
-    public void setVisibility(Visibility visibility) {
+    public void setVisibility(@NonNull Visibility visibility) {
         if (visibility != mVisibility || visibility != mScheduledVisibility) {
             mScheduledVisibility = visibility;
             invalidateMeasure();
@@ -267,7 +281,7 @@
     }
 
     @Override
-    public boolean suitableForTransition(Operation o) {
+    public boolean suitableForTransition(@NonNull Operation o) {
         if (!(o instanceof Component)) {
             return false;
         }
@@ -291,7 +305,7 @@
 
     @Override
     public void measure(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
@@ -358,16 +372,27 @@
         return x >= lx1 && x < lx2 && y >= ly1 && y < ly2;
     }
 
-    public void onClick(RemoteContext context, CoreDocument document, float x, float y) {
+    public float getScrollX() {
+        return 0;
+    }
+
+    public float getScrollY() {
+        return 0;
+    }
+
+    public void onClick(
+            @NonNull RemoteContext context, @NonNull CoreDocument document, float x, float y) {
         if (!contains(x, y)) {
             return;
         }
+        float cx = x - getScrollX();
+        float cy = y - getScrollY();
         for (Operation op : mList) {
             if (op instanceof Component) {
-                ((Component) op).onClick(context, document, x, y);
+                ((Component) op).onClick(context, document, cx, cy);
             }
             if (op instanceof ClickHandler) {
-                ((ClickHandler) op).onClick(context, document, this, x, y);
+                ((ClickHandler) op).onClick(context, document, this, cx, cy);
             }
         }
     }
@@ -376,12 +401,14 @@
         if (!contains(x, y)) {
             return;
         }
+        float cx = x - getScrollX();
+        float cy = y - getScrollY();
         for (Operation op : mList) {
             if (op instanceof Component) {
-                ((Component) op).onTouchDown(context, document, x, y);
+                ((Component) op).onTouchDown(context, document, cx, cy);
             }
             if (op instanceof TouchHandler) {
-                ((TouchHandler) op).onTouchDown(context, document, this, x, y);
+                ((TouchHandler) op).onTouchDown(context, document, this, cx, cy);
             }
         }
     }
@@ -391,12 +418,14 @@
         if (!force && !contains(x, y)) {
             return;
         }
+        float cx = x - getScrollX();
+        float cy = y - getScrollY();
         for (Operation op : mList) {
             if (op instanceof Component) {
-                ((Component) op).onTouchUp(context, document, x, y, force);
+                ((Component) op).onTouchUp(context, document, cx, cy, force);
             }
             if (op instanceof TouchHandler) {
-                ((TouchHandler) op).onTouchUp(context, document, this, x, y);
+                ((TouchHandler) op).onTouchUp(context, document, this, cx, cy);
             }
         }
     }
@@ -406,12 +435,31 @@
         if (!force && !contains(x, y)) {
             return;
         }
+        float cx = x - getScrollX();
+        float cy = y - getScrollY();
         for (Operation op : mList) {
             if (op instanceof Component) {
-                ((Component) op).onTouchCancel(context, document, x, y, force);
+                ((Component) op).onTouchCancel(context, document, cx, cy, force);
             }
             if (op instanceof TouchHandler) {
-                ((TouchHandler) op).onTouchCancel(context, document, this, x, y);
+                ((TouchHandler) op).onTouchCancel(context, document, this, cx, cy);
+            }
+        }
+    }
+
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, float x, float y, boolean force) {
+        if (!force && !contains(x, y)) {
+            return;
+        }
+        float cx = x - getScrollX();
+        float cy = y - getScrollY();
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                ((Component) op).onTouchDrag(context, document, cx, cy, force);
+            }
+            if (op instanceof TouchHandler) {
+                ((TouchHandler) op).onTouchDrag(context, document, this, cx, cy);
             }
         }
     }
@@ -480,7 +528,7 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         // nothing
     }
 
@@ -502,7 +550,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         StringBuilder builder = new StringBuilder();
         builder.append(indent);
         builder.append(toString());
@@ -602,8 +650,8 @@
         if (mPreTranslate != null) {
             mPreTranslate.paint(context);
         }
-        Component prev = context.getContext().lastComponent;
-        context.getContext().lastComponent = this;
+        Component prev = context.getContext().mLastComponent;
+        context.getContext().mLastComponent = this;
         context.save();
         context.translate(mX, mY);
         if (context.isVisualDebug()) {
@@ -618,7 +666,7 @@
             }
         }
         context.restore();
-        context.getContext().lastComponent = prev;
+        context.getContext().mLastComponent = prev;
     }
 
     public boolean applyAnimationAsNeeded(@NonNull PaintContext context) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
index f370e20..476b73c 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -40,13 +39,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // nothing
     }
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
@@ -67,7 +66,7 @@
         return 1 + 4 + 4 + 4;
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         operations.add(new ComponentEnd());
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
index f250d9a..def9f78 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
@@ -19,7 +19,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -96,12 +95,12 @@
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // nothing
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
index bb43119..d617007 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 
 /**
@@ -22,5 +24,5 @@
  * measured. Eg borders, background, clips, etc.
  */
 public interface DecoratorComponent {
-    void layout(RemoteContext context, float width, float height);
+    void layout(@NonNull RemoteContext context, float width, float height);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
index e0923dfb..0041582 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
@@ -24,7 +24,9 @@
 import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
 import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
 import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
+import com.android.internal.widget.remotecompose.core.operations.PaintData;
 import com.android.internal.widget.remotecompose.core.operations.TextData;
+import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DimensionModifierOperation;
@@ -57,12 +59,14 @@
     protected float mPaddingBottom = 0f;
 
     @NonNull protected ComponentModifiers mComponentModifiers = new ComponentModifiers();
-    @NonNull protected ArrayList<Component> mChildrenComponents = new ArrayList<>();
+
+    @NonNull
+    protected ArrayList<Component> mChildrenComponents = new ArrayList<>(); // members are not null
 
     protected boolean mChildrenHaveZIndex = false;
 
     public LayoutComponent(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -129,6 +133,9 @@
 
     public void inflate() {
         ArrayList<TextData> data = new ArrayList<>();
+        ArrayList<TouchExpression> touchExpressions = new ArrayList<>();
+        ArrayList<PaintData> paintData = new ArrayList<>();
+
         for (Operation op : mList) {
             if (op instanceof LayoutComponentContent) {
                 mContent = (LayoutComponentContent) op;
@@ -172,6 +179,10 @@
                 mComponentModifiers.add((ModifierOperation) op);
             } else if (op instanceof TextData) {
                 data.add((TextData) op);
+            } else if (op instanceof TouchExpression) {
+                touchExpressions.add((TouchExpression) op);
+            } else if (op instanceof PaintData) {
+                paintData.add((PaintData) op);
             } else {
                 // nothing
             }
@@ -179,6 +190,8 @@
 
         mList.clear();
         mList.addAll(data);
+        mList.addAll(touchExpressions);
+        mList.addAll(paintData);
         mList.add(mComponentModifiers);
         for (Component c : mChildrenComponents) {
             c.mParent = this;
@@ -255,11 +268,24 @@
     }
 
     @Override
+    public float getScrollX() {
+        return mComponentModifiers.getScrollX();
+    }
+
+    @Override
+    public float getScrollY() {
+        return mComponentModifiers.getScrollY();
+    }
+
+    @Override
     public void paintingComponent(@NonNull PaintContext context) {
-        Component prev = context.getContext().lastComponent;
-        context.getContext().lastComponent = this;
+        Component prev = context.getContext().mLastComponent;
+        context.getContext().mLastComponent = this;
         context.save();
         context.translate(mX, mY);
+        if (context.isVisualDebug()) {
+            debugBox(this, context);
+        }
         if (mGraphicsLayerModifier != null) {
             context.startGraphicsLayer((int) getWidth(), (int) getHeight());
             float scaleX = mGraphicsLayerModifier.getScaleX();
@@ -285,8 +311,8 @@
                     renderEffectId);
         }
         mComponentModifiers.paint(context);
-        float tx = mPaddingLeft;
-        float ty = mPaddingTop;
+        float tx = mPaddingLeft + getScrollX();
+        float ty = mPaddingTop + getScrollY();
         context.translate(tx, ty);
         if (mChildrenHaveZIndex) {
             // TODO -- should only sort when something has changed
@@ -305,7 +331,7 @@
         }
         context.translate(-tx, -ty);
         context.restore();
-        context.getContext().lastComponent = prev;
+        context.getContext().mLastComponent = prev;
     }
 
     /** Traverse the modifiers to compute indicated dimension */
@@ -337,7 +363,7 @@
      * @param padding output start and end padding values
      * @return padding width
      */
-    public float computeModifierDefinedPaddingWidth(float[] padding) {
+    public float computeModifierDefinedPaddingWidth(@NonNull float[] padding) {
         float s = 0f;
         float e = 0f;
         for (Operation c : mComponentModifiers.getList()) {
@@ -381,7 +407,7 @@
      * @param padding output top and bottom padding values
      * @return padding height
      */
-    public float computeModifierDefinedPaddingHeight(float[] padding) {
+    public float computeModifierDefinedPaddingHeight(@NonNull float[] padding) {
         float t = 0f;
         float b = 0f;
         for (Operation c : mComponentModifiers.getList()) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
index 0a085b4..7eea885 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
@@ -18,6 +18,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -35,7 +36,7 @@
             float y,
             float width,
             float height,
-            Component parent,
+            @Nullable Component parent,
             int animationId) {
         super(parent, componentId, animationId, x, y, width, height);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ListActionsOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ListActionsOperation.java
index c4df075..df960e4 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ListActionsOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ListActionsOperation.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.PaintContext;
@@ -59,8 +61,9 @@
         }
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopEnd.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopEnd.java
index c90077b..71de285 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopEnd.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopEnd.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -40,13 +39,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // nothing
     }
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
@@ -63,7 +62,7 @@
         buffer.start(id());
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         operations.add(new LoopEnd());
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java
index eeaeafd..d88382d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -70,7 +69,7 @@
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/OperationsListEnd.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/OperationsListEnd.java
index bd8d1f0..ca79003 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/OperationsListEnd.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/OperationsListEnd.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -40,13 +39,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // nothing
     }
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
@@ -63,7 +62,7 @@
         buffer.start(id());
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         operations.add(new OperationsListEnd());
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
index 524ae59..85c7153 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
@@ -18,6 +18,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -45,13 +46,18 @@
             float y,
             float width,
             float height,
-            Component parent,
+            @Nullable Component parent,
             int animationId) {
         super(parent, componentId, animationId, x, y, width, height);
     }
 
     public RootLayoutComponent(
-            int componentId, float x, float y, float width, float height, Component parent) {
+            int componentId,
+            float x,
+            float y,
+            float width,
+            float height,
+            @Nullable Component parent) {
         super(parent, componentId, -1, x, y, width, height);
     }
 
@@ -130,7 +136,7 @@
         if (!mNeedsMeasure) {
             return;
         }
-        context.lastComponent = this;
+        context.mLastComponent = this;
         mWidth = context.mWidth;
         mHeight = context.mHeight;
 
@@ -149,7 +155,7 @@
     @Override
     public void paint(@NonNull PaintContext context) {
         mNeedsRepaint = false;
-        context.getContext().lastComponent = this;
+        context.getContext().mLastComponent = this;
         context.save();
 
         if (mParent == null) { // root layout
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java
index 486efbd..0316f96 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java
@@ -70,6 +70,12 @@
         applyActions(context, document, component, x, y, true);
     }
 
+    @Override
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        // nothing
+    }
+
     public static String name() {
         return "TouchCancelModifier";
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java
index 5d379fe..d98911f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java
@@ -72,6 +72,12 @@
         // nothing
     }
 
+    @Override
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        // nothing
+    }
+
     public static String name() {
         return "TouchModifier";
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchHandler.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchHandler.java
index 5adfc33..ac9dd90 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchHandler.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchHandler.java
@@ -46,6 +46,18 @@
             RemoteContext context, CoreDocument document, Component component, float x, float y);
 
     /**
+     * callback for a touch move event
+     *
+     * @param context the current context
+     * @param document the current document
+     * @param component the component on which the touch has been received
+     * @param x the x position of the click in document coordinates
+     * @param y the y position of the click in document coordinates
+     */
+    void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y);
+
+    /**
      * callback for a touch cancel event
      *
      * @param context the current context
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java
index 263cc43..f6cb375 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java
@@ -70,6 +70,12 @@
         // nothing
     }
 
+    @Override
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        // nothing
+    }
+
     public static String name() {
         return "TouchUpModifier";
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
index 6036b74..b343099 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
@@ -33,40 +33,40 @@
  * <p>Handles position, size and visibility
  */
 public class AnimateMeasure {
-    long mStartTime = System.currentTimeMillis();
-    Component mComponent;
-    ComponentMeasure mOriginal;
-    ComponentMeasure mTarget;
-    int mDuration;
-    int mDurationVisibilityChange = mDuration;
-    AnimationSpec.ANIMATION mEnterAnimation = AnimationSpec.ANIMATION.FADE_IN;
-    AnimationSpec.ANIMATION mExitAnimation = AnimationSpec.ANIMATION.FADE_OUT;
-    int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
-    int mVisibilityEasingType = GeneralEasing.CUBIC_ACCELERATE;
+    private long mStartTime = System.currentTimeMillis();
+    private final @NonNull Component mComponent;
+    private final @NonNull ComponentMeasure mOriginal;
+    private final @NonNull ComponentMeasure mTarget;
+    private int mDuration;
+    private int mDurationVisibilityChange = mDuration;
+    private @NonNull AnimationSpec.ANIMATION mEnterAnimation = AnimationSpec.ANIMATION.FADE_IN;
+    private @NonNull AnimationSpec.ANIMATION mExitAnimation = AnimationSpec.ANIMATION.FADE_OUT;
+    private int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
+    private int mVisibilityEasingType = GeneralEasing.CUBIC_ACCELERATE;
 
-    float mP = 0f;
-    float mVp = 0f;
+    private float mP = 0f;
+    private float mVp = 0f;
 
     @NonNull
-    FloatAnimation mMotionEasing =
+    private FloatAnimation mMotionEasing =
             new FloatAnimation(mMotionEasingType, mDuration / 1000f, null, 0f, Float.NaN);
 
     @NonNull
-    FloatAnimation mVisibilityEasing =
+    private FloatAnimation mVisibilityEasing =
             new FloatAnimation(
                     mVisibilityEasingType, mDurationVisibilityChange / 1000f, null, 0f, Float.NaN);
 
-    ParticleAnimation mParticleAnimation;
+    private ParticleAnimation mParticleAnimation;
 
     public AnimateMeasure(
             long startTime,
             @NonNull Component component,
-            ComponentMeasure original,
+            @NonNull ComponentMeasure original,
             @NonNull ComponentMeasure target,
             int duration,
             int durationVisibilityChange,
-            AnimationSpec.ANIMATION enterAnimation,
-            AnimationSpec.ANIMATION exitAnimation,
+            @NonNull AnimationSpec.ANIMATION enterAnimation,
+            @NonNull AnimationSpec.ANIMATION exitAnimation,
             int motionEasingType,
             int visibilityEasingType) {
         this.mStartTime = startTime;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
index 47abade..6fb7059 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -36,8 +35,8 @@
     int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
     int mVisibilityDuration = 300;
     int mVisibilityEasingType = GeneralEasing.CUBIC_STANDARD;
-    ANIMATION mEnterAnimation = ANIMATION.FADE_IN;
-    ANIMATION mExitAnimation = ANIMATION.FADE_OUT;
+    @NonNull ANIMATION mEnterAnimation = ANIMATION.FADE_IN;
+    @NonNull ANIMATION mExitAnimation = ANIMATION.FADE_OUT;
 
     public AnimationSpec(
             int animationId,
@@ -45,8 +44,8 @@
             int motionEasingType,
             int visibilityDuration,
             int visibilityEasingType,
-            ANIMATION enterAnimation,
-            ANIMATION exitAnimation) {
+            @NonNull ANIMATION enterAnimation,
+            @NonNull ANIMATION exitAnimation) {
         this.mAnimationId = animationId;
         this.mMotionDuration = motionDuration;
         this.mMotionEasingType = motionEasingType;
@@ -87,10 +86,12 @@
         return mVisibilityEasingType;
     }
 
+    @NonNull
     public ANIMATION getEnterAnimation() {
         return mEnterAnimation;
     }
 
+    @NonNull
     public ANIMATION getExitAnimation() {
         return mExitAnimation;
     }
@@ -126,13 +127,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         // nothing here
     }
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
index 37d2078..64e2f004 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
@@ -34,7 +34,7 @@
             @NonNull PaintContext context,
             @NonNull Component component,
             @NonNull ComponentMeasure start,
-            ComponentMeasure end,
+            @NonNull ComponentMeasure end,
             float progress) {
         ArrayList<Particle> particles = mAllParticles.get(component.getComponentId());
         if (particles == null) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
index f3e5509..47a9421 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
@@ -18,6 +18,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -45,7 +46,7 @@
     int mVerticalPositioning;
 
     public BoxLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -60,7 +61,7 @@
     }
 
     public BoxLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             int horizontalPositioning,
@@ -104,7 +105,7 @@
 
     @Override
     public void computeWrapSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float maxWidth,
             float maxHeight,
             @NonNull MeasurePass measure,
@@ -122,7 +123,7 @@
 
     @Override
     public void computeSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
@@ -134,7 +135,7 @@
     }
 
     @Override
-    public void internalLayoutMeasure(PaintContext context, @NonNull MeasurePass measure) {
+    public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) {
         ComponentMeasure selfMeasure = measure.get(this);
         float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
         float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java
index 12ff969..476b1a66 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java
@@ -18,6 +18,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -32,7 +33,7 @@
 
 public class CanvasLayout extends BoxLayout {
     public CanvasLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -42,7 +43,7 @@
         super(parent, componentId, animationId, x, y, width, height, 0, 0);
     }
 
-    public CanvasLayout(Component parent, int componentId, int animationId) {
+    public CanvasLayout(@Nullable Component parent, int componentId, int animationId) {
         this(parent, componentId, animationId, 0, 0, 0, 0);
     }
 
@@ -103,7 +104,7 @@
     }
 
     @Override
-    public void internalLayoutMeasure(PaintContext context, @NonNull MeasurePass measure) {
+    public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) {
         ComponentMeasure selfMeasure = measure.get(this);
         float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
         float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
index 52bf4c5..68e18c6 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -53,7 +54,7 @@
     float mSpacedBy = 0f;
 
     public ColumnLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -70,7 +71,7 @@
     }
 
     public ColumnLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             int horizontalPositioning,
@@ -121,7 +122,7 @@
 
     @Override
     public void computeWrapSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float maxWidth,
             float maxHeight,
             @NonNull MeasurePass measure,
@@ -145,7 +146,7 @@
 
     @Override
     public void computeSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
@@ -164,7 +165,17 @@
     }
 
     @Override
-    public void internalLayoutMeasure(PaintContext context, @NonNull MeasurePass measure) {
+    public float intrinsicHeight() {
+        float height = computeModifierDefinedHeight();
+        float componentHeights = 0f;
+        for (Component c : mChildrenComponents) {
+            componentHeights += c.intrinsicHeight();
+        }
+        return Math.max(height, componentHeights);
+    }
+
+    @Override
+    public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) {
         ComponentMeasure selfMeasure = measure.get(this);
         DebugLog.s(
                 () ->
@@ -188,6 +199,16 @@
         float childrenWidth = 0f;
         float childrenHeight = 0f;
 
+        if (mComponentModifiers.hasHorizontalScroll()) {
+            selfWidth =
+                    mComponentModifiers.getHorizontalScrollDimension()
+                            - mPaddingLeft
+                            - mPaddingRight;
+        }
+        if (mComponentModifiers.hasVerticalScroll()) {
+            selfHeight =
+                    mComponentModifiers.getVerticalScrollDimension() - mPaddingTop - mPaddingBottom;
+        }
         boolean hasWeights = false;
         float totalWeights = 0f;
         for (Component child : mChildrenComponents) {
@@ -286,6 +307,7 @@
                 ty = verticalGap / 2f;
                 break;
         }
+
         for (Component child : mChildrenComponents) {
             ComponentMeasure childMeasure = measure.get(child);
             switch (mHorizontalPositioning) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
index 0c4d24a..3b5aaf3 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
@@ -16,6 +16,7 @@
 package com.android.internal.widget.remotecompose.core.operations.layout.managers;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.PaintContext;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
@@ -32,7 +33,7 @@
     @NonNull Size mCachedWrapSize = new Size(0f, 0f);
 
     public LayoutManager(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -43,24 +44,46 @@
     }
 
     /** Implemented by subclasses to provide a layout/measure pass */
-    public void internalLayoutMeasure(PaintContext context, MeasurePass measure) {
+    public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) {
         // nothing here
     }
 
     /** Subclasses can implement this to provide wrap sizing */
     public void computeWrapSize(
-            PaintContext context, float maxWidth, float maxHeight, MeasurePass measure, Size size) {
+            @NonNull PaintContext context,
+            float maxWidth,
+            float maxHeight,
+            @NonNull MeasurePass measure,
+            @NonNull Size size) {
         // nothing here
     }
 
+    @Override
+    public float intrinsicHeight() {
+        float height = computeModifierDefinedHeight();
+        for (Component c : mChildrenComponents) {
+            height = Math.max(c.intrinsicHeight(), height);
+        }
+        return height;
+    }
+
+    @Override
+    public float intrinsicWidth() {
+        float width = computeModifierDefinedWidth();
+        for (Component c : mChildrenComponents) {
+            width = Math.max(c.intrinsicWidth(), width);
+        }
+        return width;
+    }
+
     /** Subclasses can implement this when not in wrap sizing */
     public void computeSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
             float maxHeight,
-            MeasurePass measure) {
+            @NonNull MeasurePass measure) {
         // nothing here
     }
 
@@ -99,7 +122,7 @@
     /** Base implementation of the measure resolution */
     @Override
     public void measure(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
@@ -112,7 +135,13 @@
                 Math.min(maxHeight, computeModifierDefinedHeight() - mMarginTop - mMarginBottom);
         float insetMaxWidth = maxWidth - mMarginLeft - mMarginRight;
         float insetMaxHeight = maxHeight - mMarginTop - mMarginBottom;
-        if (mWidthModifier.isWrap() || mHeightModifier.isWrap()) {
+        if (mWidthModifier.isIntrinsicMin()) {
+            maxWidth = intrinsicWidth();
+        }
+        if (mHeightModifier.isIntrinsicMin()) {
+            maxHeight = intrinsicHeight();
+        }
+        if (mWidthModifier.isWrap() || mHeightModifier.isWrap()) { // TODO: potential npe -- bbade@
             mCachedWrapSize.setWidth(0f);
             mCachedWrapSize.setHeight(0f);
             computeWrapSize(context, maxWidth, maxHeight, measure, mCachedWrapSize);
@@ -129,7 +158,7 @@
             measuredWidth = Math.max(measuredWidth, minWidth);
             measuredWidth = Math.min(measuredWidth, insetMaxWidth);
         }
-        if (isInVerticalFill()) {
+        if (isInVerticalFill()) { // todo: potential npe -- bbade@
             measuredHeight = insetMaxHeight;
         } else if (mHeightModifier.hasWeight()) {
             measuredHeight = Math.max(measuredHeight, computeModifierDefinedHeight());
@@ -146,7 +175,23 @@
         measuredWidth = Math.min(measuredWidth, insetMaxWidth);
         measuredHeight = Math.min(measuredHeight, insetMaxHeight);
         if (!hasWrap) {
-            computeSize(context, 0f, measuredWidth, 0f, measuredHeight, measure);
+            if (hasHorizontalScroll()) {
+                mCachedWrapSize.setWidth(0f);
+                mCachedWrapSize.setHeight(0f);
+                computeWrapSize(context, Float.MAX_VALUE, maxHeight, measure, mCachedWrapSize);
+                float w = mCachedWrapSize.getWidth();
+                computeSize(context, 0f, w, 0, measuredHeight, measure);
+                mComponentModifiers.setHorizontalScrollDimension(measuredWidth, w);
+            } else if (hasVerticalScroll()) {
+                mCachedWrapSize.setWidth(0f);
+                mCachedWrapSize.setHeight(0f);
+                computeWrapSize(context, maxWidth, Float.MAX_VALUE, measure, mCachedWrapSize);
+                float h = mCachedWrapSize.getHeight();
+                computeSize(context, 0f, measuredWidth, 0, h, measure);
+                mComponentModifiers.setVerticalScrollDimension(measuredHeight, h);
+            } else {
+                computeSize(context, 0f, measuredWidth, 0f, measuredHeight, measure);
+            }
         }
 
         if (mContent != null) {
@@ -168,6 +213,14 @@
         internalLayoutMeasure(context, measure);
     }
 
+    private boolean hasHorizontalScroll() {
+        return mComponentModifiers.hasHorizontalScroll();
+    }
+
+    private boolean hasVerticalScroll() {
+        return mComponentModifiers.hasVerticalScroll();
+    }
+
     /** basic layout of internal components */
     @Override
     public void layout(@NonNull RemoteContext context, @NonNull MeasurePass measure) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
index a366dc8..0ce634f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -51,7 +52,7 @@
     float mSpacedBy = 0f;
 
     public RowLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -68,7 +69,7 @@
     }
 
     public RowLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             int horizontalPositioning,
@@ -119,7 +120,7 @@
 
     @Override
     public void computeWrapSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float maxWidth,
             float maxHeight,
             @NonNull MeasurePass measure,
@@ -143,7 +144,7 @@
 
     @Override
     public void computeSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
@@ -162,7 +163,17 @@
     }
 
     @Override
-    public void internalLayoutMeasure(PaintContext context, @NonNull MeasurePass measure) {
+    public float intrinsicWidth() {
+        float width = computeModifierDefinedWidth();
+        float componentWidths = 0f;
+        for (Component c : mChildrenComponents) {
+            componentWidths += c.intrinsicWidth();
+        }
+        return Math.max(width, componentWidths);
+    }
+
+    @Override
+    public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) {
         ComponentMeasure selfMeasure = measure.get(this);
         DebugLog.s(
                 () ->
@@ -186,6 +197,17 @@
         float childrenWidth = 0f;
         float childrenHeight = 0f;
 
+        if (mComponentModifiers.hasHorizontalScroll()) {
+            selfWidth =
+                    mComponentModifiers.getHorizontalScrollDimension()
+                            - mPaddingLeft
+                            - mPaddingRight;
+        }
+        if (mComponentModifiers.hasVerticalScroll()) {
+            selfHeight =
+                    mComponentModifiers.getVerticalScrollDimension() - mPaddingTop - mPaddingBottom;
+        }
+
         boolean hasWeights = false;
         float totalWeights = 0f;
         for (Component child : mChildrenComponents) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java
index e47ffde..73a104b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java
@@ -16,6 +16,7 @@
 package com.android.internal.widget.remotecompose.core.operations.layout.managers;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -60,7 +61,7 @@
     public boolean inTransition = false;
 
     public StateLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -132,21 +133,21 @@
 
     @Override
     public void computeSize(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
             float maxHeight,
-            MeasurePass measure) {
+            @NonNull MeasurePass measure) {
         LayoutManager layout = getLayout(currentLayoutIndex);
         layout.computeSize(context, minWidth, maxWidth, minHeight, maxHeight, measure);
     }
 
     @Override
     public void internalLayoutMeasure(
-            PaintContext context,
+            @NonNull PaintContext context,
             // layoutInfo: LayoutInfo,
-            MeasurePass measure) {
+            @NonNull MeasurePass measure) {
         LayoutManager layout = getLayout(currentLayoutIndex);
         //        layout.internalLayoutMeasure(context, layoutInfo, measure)
         layout.internalLayoutMeasure(context, measure);
@@ -155,13 +156,18 @@
     /** Subclasses can implement this to provide wrap sizing */
     @Override
     public void computeWrapSize(
-            PaintContext context, float maxWidth, float maxHeight, MeasurePass measure, Size size) {
+            @NonNull PaintContext context,
+            float maxWidth,
+            float maxHeight,
+            @NonNull MeasurePass measure,
+            @NonNull Size size) {
         LayoutManager layout = getLayout(currentLayoutIndex);
         layout.computeWrapSize(context, maxWidth, maxHeight, measure, size);
     }
 
     @Override
-    public void onClick(RemoteContext context, CoreDocument document, float x, float y) {
+    public void onClick(
+            @NonNull RemoteContext context, @NonNull CoreDocument document, float x, float y) {
         if (!contains(x, y)) {
             return;
         }
@@ -352,7 +358,7 @@
         }
     }
 
-    public LayoutManager getLayout(int idx) {
+    public @NonNull LayoutManager getLayout(int idx) {
         int index = 0;
         for (Component pane : mChildrenComponents) {
             if (pane instanceof LayoutComponent) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java
index 8aa7712..a527e5a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java
@@ -19,6 +19,7 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -52,8 +53,9 @@
     private float mTextX;
     private float mTextY;
     private float mTextW;
+    private float mTextH;
 
-    private String mCachedString = "";
+    @Nullable private String mCachedString = "";
 
     @Override
     public void registerListening(@NonNull RemoteContext context) {
@@ -89,7 +91,7 @@
     }
 
     public TextLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             float x,
@@ -114,7 +116,7 @@
     }
 
     public TextLayout(
-            Component parent,
+            @Nullable Component parent,
             int componentId,
             int animationId,
             int textId,
@@ -162,6 +164,9 @@
         mPaint.setTextSize(mFontSize);
         mPaint.setTextStyle(mType, (int) mFontWeight, mFontStyle == 1);
         context.applyPaint(mPaint);
+        if (mCachedString == null) {
+            return;
+        }
         int length = mCachedString.length();
         context.drawTextRun(mTextId, 0, length, 0, 0, mTextX, mTextY, false);
         if (DEBUG) {
@@ -241,7 +246,7 @@
             @NonNull PaintContext context,
             float maxWidth,
             float maxHeight,
-            MeasurePass measure,
+            @NonNull MeasurePass measure,
             @NonNull Size size) {
         context.savePaint();
         mPaint.reset();
@@ -250,6 +255,9 @@
         context.applyPaint(mPaint);
         float[] bounds = new float[4];
         int flags = PaintContext.TEXT_MEASURE_FONT_HEIGHT;
+        if (mCachedString == null) {
+            return;
+        }
         context.getTextBounds(mTextId, 0, mCachedString.length(), flags, bounds);
         context.restorePaint();
         float w = bounds[2] - bounds[0];
@@ -259,6 +267,17 @@
         size.setHeight(h);
         mTextY = -bounds[1];
         mTextW = w;
+        mTextH = h;
+    }
+
+    @Override
+    public float intrinsicHeight() {
+        return mTextH;
+    }
+
+    @Override
+    public float intrinsicWidth() {
+        return mTextW;
     }
 
     @NonNull
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
index 426e023..82f23cd 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
@@ -26,7 +26,7 @@
     float mY;
     float mW;
     float mH;
-    Component.Visibility mVisibility = Component.Visibility.VISIBLE;
+    @NonNull Component.Visibility mVisibility = Component.Visibility.VISIBLE;
 
     public void setX(float value) {
         mX = value;
@@ -60,16 +60,16 @@
         return mH;
     }
 
-    public Component.Visibility getVisibility() {
+    public @NonNull Component.Visibility getVisibility() {
         return mVisibility;
     }
 
-    public void setVisibility(Component.Visibility visibility) {
+    public void setVisibility(@NonNull Component.Visibility visibility) {
         mVisibility = visibility;
     }
 
     public ComponentMeasure(
-            int id, float x, float y, float w, float h, Component.Visibility visibility) {
+            int id, float x, float y, float w, float h, @NonNull Component.Visibility visibility) {
         this.mId = id;
         this.mX = x;
         this.mY = y;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
index b48c2d5..fbf2784 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout.measure;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.PaintContext;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 
@@ -26,15 +28,15 @@
      * does not apply the measure to the component.
      */
     void measure(
-            PaintContext context,
+            @NonNull PaintContext context,
             float minWidth,
             float maxWidth,
             float minHeight,
             float maxHeight,
-            MeasurePass measure);
+            @NonNull MeasurePass measure);
 
     /** Apply a given measure to the component */
-    void layout(RemoteContext context, MeasurePass measure);
+    void layout(@NonNull RemoteContext context, @NonNull MeasurePass measure);
 
     /**
      * Return true if the component needs to be remeasured
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
index 112ab1b..5cfb1b4 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
@@ -43,7 +43,7 @@
         return mList.containsKey(id);
     }
 
-    public ComponentMeasure get(@NonNull Component c) {
+    public @NonNull ComponentMeasure get(@NonNull Component c) {
         if (!mList.containsKey(c.getComponentId())) {
             ComponentMeasure measure =
                     new ComponentMeasure(
@@ -54,7 +54,7 @@
         return mList.get(c.getComponentId());
     }
 
-    public ComponentMeasure get(int id) {
+    public @NonNull ComponentMeasure get(int id) {
         if (!mList.containsKey(id)) {
             ComponentMeasure measure =
                     new ComponentMeasure(id, 0f, 0f, 0f, 0f, Component.Visibility.GONE);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
index 76a97ca..71d2ba6 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
@@ -98,7 +98,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         this.mWidth = width;
         this.mHeight = height;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
index d48a9c7..0707cd6 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
@@ -124,7 +124,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         this.mWidth = width;
         this.mHeight = height;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
index 78b51c3..e05b027 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
@@ -40,7 +40,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         this.mWidth = width;
         this.mHeight = height;
     }
@@ -68,7 +68,7 @@
         buffer.start(OP_CODE);
     }
 
-    public static void read(WireBuffer buffer, @NonNull List<Operation> operations) {
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
         operations.add(new ClipRectModifierOperation());
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
index 011d7ed..d11f26f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
@@ -62,7 +62,7 @@
     }
 
     @Override
-    public void write(WireBuffer buffer) {
+    public void write(@NonNull WireBuffer buffer) {
         // nothing
     }
 
@@ -73,7 +73,7 @@
         }
     }
 
-    public void add(ModifierOperation operation) {
+    public void add(@NonNull ModifierOperation operation) {
         mList.add(operation);
     }
 
@@ -109,7 +109,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         float w = width;
         float h = height;
         for (ModifierOperation op : mList) {
@@ -126,13 +126,17 @@
         }
     }
 
-    public void addAll(ArrayList<ModifierOperation> operations) {
+    public void addAll(@NonNull ArrayList<ModifierOperation> operations) {
         mList.addAll(operations);
     }
 
     @Override
     public void onClick(
-            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+            @NonNull RemoteContext context,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
+            float x,
+            float y) {
         for (ModifierOperation op : mList) {
             if (op instanceof ClickHandler) {
                 ((ClickHandler) op).onClick(context, document, component, x, y);
@@ -169,4 +173,110 @@
             }
         }
     }
+
+    @Override
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        for (ModifierOperation op : mList) {
+            if (op instanceof TouchHandler) {
+                ((TouchHandler) op).onTouchDrag(context, document, component, x, y);
+            }
+        }
+    }
+
+    public boolean hasHorizontalScroll() {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isHorizontalScroll()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public boolean hasVerticalScroll() {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isVerticalScroll()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public float getScrollX() {
+        float scroll = 0;
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isHorizontalScroll()) {
+                    scroll = Math.min(scroll, scrollModifier.getScrollX());
+                }
+            }
+        }
+        return scroll;
+    }
+
+    public float getScrollY() {
+        float scroll = 0;
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isVerticalScroll()) {
+                    scroll = Math.min(scroll, scrollModifier.getScrollY());
+                }
+            }
+        }
+        return scroll;
+    }
+
+    public void setHorizontalScrollDimension(float hostDimension, float contentDimension) {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isHorizontalScroll()) {
+                    scrollModifier.setHorizontalScrollDimension(hostDimension, contentDimension);
+                }
+            }
+        }
+    }
+
+    public void setVerticalScrollDimension(float hostDimension, float contentDimension) {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isVerticalScroll()) {
+                    scrollModifier.setVerticalScrollDimension(hostDimension, contentDimension);
+                }
+            }
+        }
+    }
+
+    public float getHorizontalScrollDimension() {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isHorizontalScroll()) {
+                    return scrollModifier.getContentDimension();
+                }
+            }
+        }
+        return 0f;
+    }
+
+    public float getVerticalScrollDimension() {
+        for (ModifierOperation op : mList) {
+            if (op instanceof ScrollModifierOperation) {
+                ScrollModifierOperation scrollModifier = (ScrollModifierOperation) op;
+                if (scrollModifier.isVerticalScroll()) {
+                    return scrollModifier.getContentDimension();
+                }
+            }
+        }
+        return 0f;
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java
index 26e737b3..471db0b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java
@@ -63,16 +63,16 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     public static void apply(@NonNull WireBuffer buffer, int valueId) {
         buffer.start(OP_CODE);
@@ -114,10 +114,10 @@
         }
     }
 
-    public void setParent(LayoutComponent parent) {
+    public void setParent(@Nullable LayoutComponent parent) {
         mParent = parent;
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {}
+    public void layout(@NonNull RemoteContext context, float width, float height) {}
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
index 3c2d85c..b9324f0 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
@@ -16,7 +16,6 @@
 package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.VariableSupport;
@@ -57,16 +56,16 @@
         }
     }
 
-    Type mType = Type.EXACT;
+    @NonNull Type mType = Type.EXACT;
     float mValue = Float.NaN;
     float mOutValue = Float.NaN;
 
-    public DimensionModifierOperation(Type type, float value) {
+    public DimensionModifierOperation(@NonNull Type type, float value) {
         mType = type;
         mOutValue = mValue = value;
     }
 
-    public DimensionModifierOperation(Type type) {
+    public DimensionModifierOperation(@NonNull Type type) {
         this(type, Float.NaN);
     }
 
@@ -115,7 +114,15 @@
         return mType == Type.FILL;
     }
 
-    public Type getType() {
+    public boolean isIntrinsicMin() {
+        return mType == Type.INTRINSIC_MIN;
+    }
+
+    public boolean isIntrinsicMax() {
+        return mType == Type.INTRINSIC_MAX;
+    }
+
+    public @NonNull Type getType() {
         return mType;
     }
 
@@ -143,11 +150,11 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java
index 2b30382..571e554 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java
@@ -18,6 +18,8 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.PaintContext;
@@ -175,8 +177,9 @@
         serializer.append(indent, "GRAPHICS_LAYER = [" + mScaleX + ", " + mScaleY + "]");
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
index 97c76c0..7bb4a75 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
@@ -59,11 +59,11 @@
         apply(buffer, mType.ordinal(), mValue);
     }
 
-    public HeightModifierOperation(Type type, float value) {
+    public HeightModifierOperation(@NonNull Type type, float value) {
         super(type, value);
     }
 
-    public HeightModifierOperation(Type type) {
+    public HeightModifierOperation(@NonNull Type type) {
         super(type);
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java
index 836321f..d239bc8 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -63,22 +62,22 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     @Override
     public void runAction(
             @NonNull RemoteContext context,
-            CoreDocument document,
-            Component component,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
             float x,
             float y) {
         context.runAction(mActionId, "");
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java
index e97e897..3268e5e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -39,6 +38,7 @@
     public static final int FLOAT_TYPE = 0;
     public static final int INT_TYPE = 1;
     public static final int STRING_TYPE = 2;
+    public static final int FLOAT_ARRAY_TYPE = 3;
     public static final int NONE_TYPE = -1;
 
     int mTextId = -1;
@@ -72,22 +72,22 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     @Override
     public void runAction(
             @NonNull RemoteContext context,
-            CoreDocument document,
-            Component component,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
             float x,
             float y) {
         Object value = null;
@@ -98,6 +98,8 @@
                 value = context.mRemoteComposeState.getFromId(mValueId);
             } else if (mType == FLOAT_TYPE) {
                 value = context.mRemoteComposeState.getFloat(mValueId);
+            } else if (mType == FLOAT_ARRAY_TYPE) {
+                value = context.mRemoteComposeState.getFloats(mValueId);
             }
         }
         context.runNamedAction(mTextId, value);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
index 50f098e..8f08f14 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
@@ -15,10 +15,12 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
 
 /** Represents a modifier */
 public interface ModifierOperation extends Operation {
-    void serializeToString(int indent, StringSerializer serializer);
+    void serializeToString(int indent, @NonNull StringSerializer serializer);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java
index 65fe345..8c07059 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java
@@ -17,6 +17,8 @@
 
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.PaintContext;
@@ -67,8 +69,9 @@
         serializer.append(indent, "OFFSET = [" + mX + ", " + mY + "]");
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
index ed5522e..2b6621e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -92,11 +91,11 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
index 6218dd5..3fefc58 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
@@ -96,7 +96,7 @@
     }
 
     @Override
-    public void layout(RemoteContext context, float width, float height) {
+    public void layout(@NonNull RemoteContext context, float width, float height) {
         this.mWidth = width;
         this.mHeight = height;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java
new file mode 100644
index 0000000..8dcfed9
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java
@@ -0,0 +1,227 @@
+/*
+ * 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.widget.remotecompose.core.operations.layout.modifiers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
+
+import android.annotation.NonNull;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.TouchHandler;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/** Represents a scroll modifier. */
+public class ScrollModifierOperation extends DecoratorModifierOperation implements TouchHandler {
+    private static final int OP_CODE = Operations.MODIFIER_SCROLL;
+    public static final String CLASS_NAME = "ScrollModifierOperation";
+
+    private final float mPositionExpression;
+    private final float mMax;
+    private final float mNotchMax;
+
+    float mWidth = 0;
+    float mHeight = 0;
+
+    int mDirection;
+
+    float mTouchDownX;
+    float mTouchDownY;
+
+    float mInitialScrollX;
+    float mInitialScrollY;
+
+    float mScrollX;
+    float mScrollY;
+
+    float mMaxScrollX;
+    float mMaxScrollY;
+
+    float mHostDimension;
+    float mContentDimension;
+
+    public ScrollModifierOperation(int direction, float position, float max, float notchMax) {
+        this.mDirection = direction;
+        this.mPositionExpression = position;
+        this.mMax = max;
+        this.mNotchMax = notchMax;
+    }
+
+    public boolean isVerticalScroll() {
+        return mDirection == 0;
+    }
+
+    public boolean isHorizontalScroll() {
+        return mDirection != 0;
+    }
+
+    public float getScrollX() {
+        return mScrollX;
+    }
+
+    public float getScrollY() {
+        return mScrollY;
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        RootLayoutComponent root = context.getDocument().getRootLayoutComponent();
+        if (root != null) {
+            root.setHasTouchListeners(true);
+        }
+        super.apply(context);
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        apply(buffer, mDirection, mPositionExpression, mMax, mNotchMax);
+    }
+
+    // @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "SCROLL = [" + mDirection + "]");
+    }
+
+    @NonNull
+    @Override
+    public String deepToString(@NonNull String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        float position =
+                context.getContext()
+                        .mRemoteComposeState
+                        .getFloat(Utils.idFromNan(mPositionExpression));
+
+        if (mDirection == 0) {
+            mScrollY = -position;
+        } else {
+            mScrollX = -position;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ScrollModifierOperation(" + mDirection + ")";
+    }
+
+    public static String name() {
+        return CLASS_NAME;
+    }
+
+    public static int id() {
+        return OP_CODE;
+    }
+
+    public static void apply(
+            WireBuffer buffer, int direction, float position, float max, float notchMax) {
+        buffer.start(OP_CODE);
+        buffer.writeInt(direction);
+        buffer.writeFloat(position);
+        buffer.writeFloat(max);
+        buffer.writeFloat(notchMax);
+    }
+
+    public static void read(WireBuffer buffer, List<Operation> operations) {
+        int direction = buffer.readInt();
+        float position = buffer.readFloat();
+        float max = buffer.readFloat();
+        float notchMax = buffer.readFloat();
+        operations.add(new ScrollModifierOperation(direction, position, max, notchMax));
+    }
+
+    public static void documentation(DocumentationBuilder doc) {
+        doc.operation("Modifier Operations", OP_CODE, CLASS_NAME)
+                .description("define a Scroll Modifier")
+                .field(INT, "direction", "");
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        mWidth = width;
+        mHeight = height;
+        if (mDirection == 0) { // VERTICAL
+            context.loadFloat(Utils.idFromNan(mMax), mMaxScrollY);
+            context.loadFloat(Utils.idFromNan(mNotchMax), mContentDimension);
+        } else {
+            context.loadFloat(Utils.idFromNan(mMax), mMaxScrollX);
+            context.loadFloat(Utils.idFromNan(mNotchMax), mContentDimension);
+        }
+    }
+
+    @Override
+    public void onTouchDown(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        mTouchDownX = x;
+        mTouchDownY = y;
+        mInitialScrollX = mScrollX;
+        mInitialScrollY = mScrollY;
+        document.appliedTouchOperation(component);
+    }
+
+    @Override
+    public void onTouchUp(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        // If not using touch expression, should add velocity decay here
+    }
+
+    @Override
+    public void onTouchDrag(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {
+        float dx = x - mTouchDownX;
+        float dy = y - mTouchDownY;
+
+        if (!Utils.isVariable(mPositionExpression)) {
+            if (mDirection == 0) {
+                mScrollY = Math.max(-mMaxScrollY, Math.min(0, mInitialScrollY + dy));
+            } else {
+                mScrollX = Math.max(-mMaxScrollX, Math.min(0, mInitialScrollX + dx));
+            }
+        }
+    }
+
+    @Override
+    public void onTouchCancel(
+            RemoteContext context, CoreDocument document, Component component, float x, float y) {}
+
+    public void setHorizontalScrollDimension(float hostDimension, float contentDimension) {
+        mHostDimension = hostDimension;
+        mContentDimension = contentDimension;
+        mMaxScrollX = contentDimension - hostDimension;
+    }
+
+    public void setVerticalScrollDimension(float hostDimension, float contentDimension) {
+        mHostDimension = hostDimension;
+        mContentDimension = contentDimension;
+        mMaxScrollY = contentDimension - hostDimension;
+    }
+
+    public float getContentDimension() {
+        return mContentDimension;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java
index 29ec828..a97fcff 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java
@@ -18,6 +18,8 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
@@ -59,8 +61,9 @@
     @Override
     public void apply(RemoteContext context) {}
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java
new file mode 100644
index 0000000..41586b4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java
@@ -0,0 +1,105 @@
+/*
+ * 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.widget.remotecompose.core.operations.layout.modifiers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
+
+import android.annotation.NonNull;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.layout.ActionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/** Apply a value change on an integer variable. */
+public class ValueFloatExpressionChangeActionOperation implements ActionOperation {
+    private static final int OP_CODE = Operations.VALUE_FLOAT_EXPRESSION_CHANGE_ACTION;
+
+    int mTargetValueId = -1;
+    int mValueExpressionId = -1;
+
+    public ValueFloatExpressionChangeActionOperation(int id, int valueId) {
+        mTargetValueId = id;
+        mValueExpressionId = valueId;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "ValueFloatExpressionChangeActionOperation(" + mTargetValueId + ")";
+    }
+
+    @NonNull
+    public String serializedName() {
+        return "VALUE_FLOAT_EXPRESSION_CHANGE";
+    }
+
+    @Override
+    public void serializeToString(int indent, @NonNull StringSerializer serializer) {
+        serializer.append(
+                indent, serializedName() + " = " + mTargetValueId + " -> " + mValueExpressionId);
+    }
+
+    @Override
+    public void apply(RemoteContext context) {}
+
+    @NonNull
+    @Override
+    public String deepToString(@NonNull String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {}
+
+    @Override
+    public void runAction(
+            @NonNull RemoteContext context,
+            @NonNull CoreDocument document,
+            Component component,
+            float x,
+            float y) {
+        document.evaluateFloatExpression(mValueExpressionId, mTargetValueId, context);
+    }
+
+    public static void apply(@NonNull WireBuffer buffer, int valueId, int value) {
+        buffer.start(OP_CODE);
+        buffer.writeInt(valueId);
+        buffer.writeInt(value);
+    }
+
+    public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+        int valueId = buffer.readInt();
+        int value = buffer.readInt();
+        operations.add(new ValueFloatExpressionChangeActionOperation(valueId, value));
+    }
+
+    public static void documentation(@NonNull DocumentationBuilder doc) {
+        doc.operation("Layout Operations", OP_CODE, "ValueIntegerExpressionChangeActionOperation")
+                .description(
+                        "ValueIntegerExpressionChange action. "
+                                + " This operation represents a value change for the given id")
+                .field(INT, "TARGET_VALUE_ID", "Value ID")
+                .field(INT, "VALUE_ID", "id of the value to be assigned to the target");
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java
index d7ce8ac..c2cd2ab 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -61,22 +60,22 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     @Override
     public void runAction(
             @NonNull RemoteContext context,
-            CoreDocument document,
-            Component component,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
             float x,
             float y) {
         context.overrideInteger(mTargetValueId, mValue);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java
index 75d13e7..43fbb85 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -62,22 +61,22 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     @Override
     public void runAction(
             @NonNull RemoteContext context,
             @NonNull CoreDocument document,
-            Component component,
+            @NonNull Component component,
             float x,
             float y) {
         document.evaluateIntExpression(mValueExpressionId, (int) mTargetValueId, context);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java
index 26d7244..1107889 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java
@@ -18,7 +18,6 @@
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.Operation;
@@ -65,22 +64,22 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(@Nullable String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
     @Override
-    public void write(WireBuffer buffer) {}
+    public void write(@NonNull WireBuffer buffer) {}
 
     @Override
     public void runAction(
             @NonNull RemoteContext context,
-            CoreDocument document,
-            Component component,
+            @NonNull CoreDocument document,
+            @NonNull Component component,
             float x,
             float y) {
         context.overrideText(mTargetValueId, mValueId);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
index e2f899c..3c757a8 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
@@ -54,7 +54,7 @@
         operations.add(op);
     }
 
-    public WidthModifierOperation(Type type, float value) {
+    public WidthModifierOperation(@NonNull Type type, float value) {
         super(type, value);
     }
 
@@ -63,7 +63,7 @@
         apply(buffer, mType.ordinal(), mValue);
     }
 
-    public WidthModifierOperation(Type type) {
+    public WidthModifierOperation(@NonNull Type type) {
         super(type);
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java
index aa20e03..82c8f34 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java
@@ -17,6 +17,8 @@
 
 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
 
+import android.annotation.NonNull;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.PaintContext;
@@ -57,8 +59,9 @@
         serializer.append(indent, "ZINDEX = [" + mValue + "]");
     }
 
+    @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return (indent != null ? indent : "") + toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
index d8e49b0..842c9c1 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
@@ -27,11 +27,11 @@
 
     public static class Node {
         @Nullable public Node parent;
-        public String name;
-        public String endString;
+        @NonNull public String name;
+        @NonNull public String endString;
         @NonNull public ArrayList<Node> list = new ArrayList<>();
 
-        public Node(@Nullable Node parent, String name) {
+        public Node(@Nullable Node parent, @NonNull String name) {
             this.parent = parent;
             this.name = name;
             this.endString = name + " DONE";
@@ -40,13 +40,13 @@
             }
         }
 
-        public void add(Node node) {
+        public void add(@NonNull Node node) {
             list.add(node);
         }
     }
 
     public static class LogNode extends Node {
-        public LogNode(Node parent, String name) {
+        public LogNode(@Nullable Node parent, @NonNull String name) {
             super(parent, name);
         }
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
index 701167a..5ec1493 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.layout.utils;
 
+import android.annotation.NonNull;
+
 /** Basic interface for a lambda (used for logging) */
 public interface StringValueSupplier {
     /**
@@ -22,5 +24,6 @@
      *
      * @return a string
      */
+    @NonNull
     String getString();
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
index e714947..07cf762 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.paint;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import com.android.internal.widget.remotecompose.core.PaintContext;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.VariableSupport;
@@ -25,8 +28,8 @@
 
 /** Paint Bundle represents a delta of changes to a paint object */
 public class PaintBundle {
-    int[] mArray = new int[200];
-    int[] mOutArray = null;
+    @NonNull int[] mArray = new int[200];
+    @Nullable int[] mOutArray = null;
     int mPos = 0;
 
     /**
@@ -35,7 +38,7 @@
      * @param paintContext
      * @param p
      */
-    public void applyPaintChange(PaintContext paintContext, PaintChanges p) {
+    public void applyPaintChange(@NonNull PaintContext paintContext, @NonNull PaintChanges p) {
         int i = 0;
         int mask = 0;
         if (mOutArray == null) {
@@ -138,12 +141,14 @@
     //        return "????" + id + "????";
     //    }
 
+    @NonNull
     private static String colorInt(int color) {
         String str = "000000000000" + Integer.toHexString(color);
         return "0x" + str.substring(str.length() - 8);
     }
 
-    private static String colorInt(int[] color) {
+    @NonNull
+    private static String colorInt(@NonNull int[] color) {
         String str = "[";
         for (int i = 0; i < color.length; i++) {
             if (i > 0) {
@@ -162,6 +167,7 @@
         return Float.toString(fValue);
     }
 
+    @NonNull
     @Override
     public String toString() {
         StringBuilder ret = new StringBuilder("\n");
@@ -244,7 +250,8 @@
         return ret.toString();
     }
 
-    private void registerFloat(int iv, RemoteContext context, VariableSupport support) {
+    private void registerFloat(
+            int iv, @NonNull RemoteContext context, @NonNull VariableSupport support) {
         float v = Float.intBitsToFloat(iv);
         if (Float.isNaN(v)) {
             context.listensTo(Utils.idFromNan(v), support);
@@ -252,7 +259,11 @@
     }
 
     int callRegisterGradient(
-            int cmd, int[] array, int i, RemoteContext context, VariableSupport support) {
+            int cmd,
+            int[] array,
+            int i,
+            @NonNull RemoteContext context,
+            @NonNull VariableSupport support) {
         int ret = i;
         int type = (cmd >> 16);
         int control = array[ret++];
@@ -343,7 +354,7 @@
         return ret;
     }
 
-    int callPrintGradient(int cmd, int[] array, int i, StringBuilder p) {
+    int callPrintGradient(int cmd, int[] array, int i, @NonNull StringBuilder p) {
         int ret = i;
         int type = (cmd >> 16);
         int tileMode = 0;
@@ -432,7 +443,7 @@
         return ret;
     }
 
-    int callSetGradient(int cmd, int[] array, int i, PaintChanges p) {
+    int callSetGradient(int cmd, @NonNull int[] array, int i, @NonNull PaintChanges p) {
         int ret = i;
         int gradientType = (cmd >> 16);
 
@@ -487,14 +498,14 @@
         return ret;
     }
 
-    public void writeBundle(WireBuffer buffer) {
+    public void writeBundle(@NonNull WireBuffer buffer) {
         buffer.writeInt(mPos);
         for (int index = 0; index < mPos; index++) {
             buffer.writeInt(mArray[index]);
         }
     }
 
-    public void readBundle(WireBuffer buffer) {
+    public void readBundle(@NonNull WireBuffer buffer) {
         int len = buffer.readInt();
         if (len <= 0 || len > 1024) {
             throw new RuntimeException("buffer corrupt paint len = " + len);
@@ -589,9 +600,9 @@
      * @param tileMode The Shader tiling mode
      */
     public void setLinearGradient(
-            int[] colors,
+            @NonNull int[] colors,
             int idMask,
-            float[] stops,
+            @Nullable float[] stops,
             float startX,
             float startY,
             float endX,
@@ -600,7 +611,7 @@
         //        int startPos = mPos;
         int len;
         mArray[mPos++] = GRADIENT | (LINEAR_GRADIENT << 16);
-        mArray[mPos++] = (idMask << 16) | (len = (colors == null) ? 0 : colors.length);
+        mArray[mPos++] = (idMask << 16) | (len = colors.length);
         for (int i = 0; i < len; i++) {
             mArray[mPos++] = colors[i];
         }
@@ -629,7 +640,12 @@
      *     spaced evenly.
      */
     public void setSweepGradient(
-            int[] colors, int idMask, float[] stops, float centerX, float centerY) {
+            @NonNull int[] colors,
+            int idMask,
+            @Nullable float[] stops, // TODO: rename positions to stops or stops to positions, but
+            // don't have both in the same file
+            float centerX,
+            float centerY) {
         int len;
         mArray[mPos++] = GRADIENT | (SWEEP_GRADIENT << 16);
         mArray[mPos++] = (idMask << 16) | (len = (colors == null) ? 0 : colors.length);
@@ -659,9 +675,9 @@
      * @param tileMode The Shader tiling mode
      */
     public void setRadialGradient(
-            int[] colors,
+            @NonNull int[] colors,
             int idMask,
-            float[] stops,
+            @Nullable float[] stops,
             float centerX,
             float centerY,
             float radius,
@@ -900,7 +916,7 @@
         mPos = 0;
     }
 
-    public static String blendModeString(int mode) {
+    public static @NonNull String blendModeString(int mode) {
         switch (mode) {
             case PaintBundle.BLEND_MODE_CLEAR:
                 return "CLEAR";
@@ -974,7 +990,7 @@
      * @param context
      * @param support
      */
-    public void registerVars(RemoteContext context, VariableSupport support) {
+    public void registerVars(@NonNull RemoteContext context, @NonNull VariableSupport support) {
         int i = 0;
         while (i < mPos) {
             int cmd = mArray[i++];
@@ -1020,7 +1036,7 @@
      *
      * @param context
      */
-    public void updateVariables(RemoteContext context) {
+    public void updateVariables(@NonNull RemoteContext context) {
         if (mOutArray == null) {
             mOutArray = Arrays.copyOf(mArray, mArray.length);
         } else {
@@ -1066,7 +1082,7 @@
         }
     }
 
-    private int fixFloatVar(int val, RemoteContext context) {
+    private int fixFloatVar(int val, @NonNull RemoteContext context) {
         float v = Float.intBitsToFloat(val);
         if (Float.isNaN(v)) {
             int id = Utils.idFromNan(v);
@@ -1075,12 +1091,13 @@
         return val;
     }
 
-    private int fixColor(int colorId, RemoteContext context) {
+    private int fixColor(int colorId, @NonNull RemoteContext context) {
         int n = context.getColor(colorId);
         return n;
     }
 
-    int updateFloatsInGradient(int cmd, int[] out, int[] array, int i, RemoteContext context) {
+    int updateFloatsInGradient(
+            int cmd, int[] out, int[] array, int i, @NonNull RemoteContext context) {
         int ret = i;
         int type = (cmd >> 16);
         int control = array[ret++];
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
index e2402be..87a6632 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.paint;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 public class PaintChangeAdapter implements PaintChanges {
 
     @Override
@@ -64,8 +67,8 @@
 
     @Override
     public void setLinearGradient(
-            int[] colorsArray,
-            float[] stopsArray,
+            @NonNull int[] colorsArray,
+            @Nullable float[] stopsArray,
             float startX,
             float startY,
             float endX,
@@ -74,8 +77,8 @@
 
     @Override
     public void setRadialGradient(
-            int[] colorsArray,
-            float[] stopsArray,
+            @NonNull int[] colorsArray,
+            @Nullable float[] stopsArray,
             float centerX,
             float centerY,
             float radius,
@@ -83,7 +86,10 @@
 
     @Override
     public void setSweepGradient(
-            int[] colorsArray, float[] stopsArray, float centerX, float centerY) {}
+            @NonNull int[] colorsArray,
+            @Nullable float[] stopsArray,
+            float centerX,
+            float centerY) {}
 
     @Override
     public void setColorFilter(int color, int mode) {}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
index 486d763..e681647 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.paint;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 /** Interface to a paint object For more details see Android Paint */
 public interface PaintChanges {
 
@@ -135,7 +138,7 @@
      * Set a linear gradient fill
      *
      * @param colorsArray
-     * @param stopsArray
+     * @param stopsArray // todo: standardize naming
      * @param startX
      * @param startY
      * @param endX
@@ -143,8 +146,8 @@
      * @param tileMode
      */
     void setLinearGradient(
-            int[] colorsArray,
-            float[] stopsArray,
+            @NonNull int[] colorsArray,
+            @Nullable float[] stopsArray,
             float startX,
             float startY,
             float endX,
@@ -155,15 +158,15 @@
      * Set a radial gradient fill
      *
      * @param colorsArray
-     * @param stopsArray
+     * @param stopsArray // todo: standardize naming
      * @param centerX
      * @param centerY
      * @param radius
      * @param tileMode
      */
     void setRadialGradient(
-            int[] colorsArray,
-            float[] stopsArray,
+            @NonNull int[] colorsArray,
+            @Nullable float[] stopsArray,
             float centerX,
             float centerY,
             float radius,
@@ -173,11 +176,12 @@
      * Set a sweep gradient fill
      *
      * @param colorsArray
-     * @param stopsArray
+     * @param stopsArray // todo: standardize naming to either "positions" or "stops"
      * @param centerX
      * @param centerY
      */
-    void setSweepGradient(int[] colorsArray, float[] stopsArray, float centerX, float centerY);
+    void setSweepGradient(
+            @NonNull int[] colorsArray, @Nullable float[] stopsArray, float centerX, float centerY);
 
     /**
      * Set Color filter mod
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java
index a808cf0..e5f6f28 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java
@@ -16,6 +16,7 @@
 package com.android.internal.widget.remotecompose.core.operations.paint;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 /** Provides a Builder pattern for a PaintBundle */
 class Painter {
@@ -173,8 +174,8 @@
             float centerX,
             float centerY,
             float radius,
-            int[] colors,
-            float[] positions,
+            @NonNull int[] colors,
+            @NonNull float[] positions,
             int tileMode) {
         mPaint.setRadialGradient(colors, 0, positions, centerX, centerY, radius, tileMode);
         return this;
@@ -193,7 +194,8 @@
      *     spaced evenly.
      */
     @NonNull
-    public Painter setSweepGradient(float centerX, float centerY, int[] colors, float[] positions) {
+    public Painter setSweepGradient(
+            float centerX, float centerY, @NonNull int[] colors, @Nullable float[] positions) {
         mPaint.setSweepGradient(colors, 0, positions, centerX, centerY);
         return this;
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
index 1c0bec7..ff6f45d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.paint;
 
+import android.annotation.NonNull;
+
+// TODO: this interface is unused. Delete it.
 public interface TextPaint {
     void setARGB(int a, int r, int g, int b);
 
@@ -28,7 +31,7 @@
 
     void setFlags(int flags);
 
-    void setFontFeatureSettings(String settings);
+    void setFontFeatureSettings(@NonNull String settings);
 
     void setHinting(int mode);
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/AnimatedFloatExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/AnimatedFloatExpression.java
index b25f4cd..e5633c7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/AnimatedFloatExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/AnimatedFloatExpression.java
@@ -18,6 +18,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
+import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.MonotonicSpline;
+
 /** high performance floating point expression evaluator used in animation */
 public class AnimatedFloatExpression {
     @NonNull static IntMap<String> sNames = new IntMap<>();
@@ -64,20 +67,37 @@
     public static final float A_SUM = asNan(OFFSET + 35);
     public static final float A_AVG = asNan(OFFSET + 36);
     public static final float A_LEN = asNan(OFFSET + 37);
-    public static final int LAST_OP = OFFSET + 37;
+    public static final float A_SPLINE = asNan(OFFSET + 38);
 
-    public static final float VAR1 = asNan(OFFSET + 38);
-    public static final float VAR2 = asNan(OFFSET + 39);
+    public static final int LAST_OP = OFFSET + 38;
+
+    public static final float VAR1 = asNan(OFFSET + 39);
+    public static final float VAR2 = asNan(OFFSET + 40);
 
     // TODO CLAMP, CBRT, DEG, RAD, EXPM1, CEIL, FLOOR
     //    private static final float FP_PI = (float) Math.PI;
     private static final float FP_TO_RAD = 57.29578f; // 180/PI
     private static final float FP_TO_DEG = 0.017453292f; // 180/PI
 
-    float[] mStack;
+    @NonNull float[] mStack = new float[0];
     @NonNull float[] mLocalStack = new float[128];
-    float[] mVar;
-    CollectionsAccess mCollectionsAccess;
+    @NonNull float[] mVar = new float[0];
+    @Nullable CollectionsAccess mCollectionsAccess;
+    IntMap<MonotonicSpline> mSplineMap = new IntMap<>();
+
+    private float getSplineValue(int arrayId, float pos) {
+        MonotonicSpline fit = mSplineMap.get(arrayId);
+        float[] f = mCollectionsAccess.getFloats(arrayId);
+        if (fit != null) {
+            if (fit.getArray() == f) { // the array has not changed.
+                return fit.getPos(pos);
+            }
+        }
+
+        fit = new MonotonicSpline(null, f);
+        mSplineMap.put(arrayId, fit);
+        return fit.getPos(pos);
+    }
 
     /**
      * is float a math operator
@@ -114,7 +134,7 @@
      * @param var
      * @return
      */
-    public float eval(float[] exp, float... var) {
+    public float eval(@NonNull float[] exp, @NonNull float... var) {
         mStack = exp;
         mVar = var;
         int sp = -1;
@@ -137,7 +157,8 @@
      * @param var
      * @return
      */
-    public float eval(CollectionsAccess ca, float[] exp, int len, float... var) {
+    public float eval(
+            @NonNull CollectionsAccess ca, @NonNull float[] exp, int len, @NonNull float... var) {
         System.arraycopy(exp, 0, mLocalStack, 0, len);
         mStack = mLocalStack;
         mVar = var;
@@ -167,7 +188,7 @@
      * @param exp
      * @return
      */
-    public float eval(CollectionsAccess ca, float[] exp, int len) {
+    public float eval(@NonNull CollectionsAccess ca, @NonNull float[] exp, int len) {
         System.arraycopy(exp, 0, mLocalStack, 0, len);
         mStack = mLocalStack;
         mCollectionsAccess = ca;
@@ -189,7 +210,7 @@
         return mStack[sp];
     }
 
-    private int dereference(CollectionsAccess ca, int id, int sp) {
+    private int dereference(@NonNull CollectionsAccess ca, int id, int sp) {
         mStack[sp] = ca.getFloatValue(id, (int) (mStack[sp]));
         return sp;
     }
@@ -202,7 +223,7 @@
      * @param var
      * @return
      */
-    public float eval(@NonNull float[] exp, int len, float... var) {
+    public float eval(@NonNull float[] exp, int len, @NonNull float... var) {
         System.arraycopy(exp, 0, mLocalStack, 0, len);
         mStack = mLocalStack;
         mVar = var;
@@ -225,13 +246,12 @@
      * @param var
      * @return
      */
-    public float evalDB(@NonNull float[] exp, float... var) {
+    public float evalDB(@NonNull float[] exp, @NonNull float... var) {
         mStack = exp;
         mVar = var;
         int sp = -1;
         for (float v : exp) {
             if (Float.isNaN(v)) {
-                System.out.print(" " + sNames.get((fromNaN(v) - OFFSET)));
                 sp = mOps[fromNaN(v) - OFFSET].eval(sp);
             } else {
                 System.out.print(" " + v);
@@ -375,12 +395,12 @@
                     return sp - 2;
                 };
         Op mCLAMP =
-                (sp) -> { // CLAMP
+                (sp) -> { // CLAMP (min, max, value)
                     mStack[sp - 2] = Math.min(Math.max(mStack[sp - 2], mStack[sp]), mStack[sp - 1]);
                     return sp - 2;
                 };
         Op mCBRT =
-                (sp) -> { // CBRT
+                (sp) -> { // CBRT is cube root
                     mStack[sp] = (float) Math.pow(mStack[sp], 1 / 3.);
                     return sp;
                 };
@@ -401,8 +421,10 @@
                 };
         Op mA_DEREF =
                 (sp) -> { // A_DEREF
-                    int id = fromNaN(mStack[sp]);
-                    mStack[sp - 1] = mCollectionsAccess.getFloatValue(id, (int) mStack[sp - 1]);
+                    Utils.log(" \n >>> DREF " + Integer.toHexString(fromNaN(mStack[sp - 1])));
+                    Utils.log(" >>> DREF " + mStack[sp] + "  " + mStack[sp - 1]);
+                    int id = fromNaN(mStack[sp - 1]);
+                    mStack[sp - 1] = mCollectionsAccess.getFloatValue(id, (int) mStack[sp]);
                     return sp - 1;
                 };
         Op mA_MAX =
@@ -420,11 +442,14 @@
                 (sp) -> { // A_MIN
                     int id = fromNaN(mStack[sp]);
                     float[] array = mCollectionsAccess.getFloats(id);
-                    float max = array[0];
-                    for (int i = 1; i < array.length; i++) {
-                        max = Math.max(max, array[i]);
+                    if (array.length == 0) {
+                        return sp;
                     }
-                    mStack[sp] = max;
+                    float min = array[0];
+                    for (int i = 1; i < array.length; i++) {
+                        min = Math.min(min, array[i]);
+                    }
+                    mStack[sp] = min;
                     return sp;
                 };
         Op mA_SUM =
@@ -455,6 +480,12 @@
                     mStack[sp] = mCollectionsAccess.getListLength(id);
                     return sp;
                 };
+        Op mA_SPLINE =
+                (sp) -> { // A_SPLINE
+                    int id = fromNaN(mStack[sp - 1]);
+                    mStack[sp - 1] = getSplineValue(id, mStack[sp]);
+                    return sp - 1;
+                };
         Op mFIRST_VAR =
                 (sp) -> { // FIRST_VAR
                     mStack[sp] = mVar[0];
@@ -510,6 +541,7 @@
             mA_SUM,
             mA_AVG,
             mA_LEN,
+            mA_SPLINE,
             mFIRST_VAR,
             mSECOND_VAR,
             mTHIRD_VAR,
@@ -558,6 +590,7 @@
         sNames.put(k++, "A_SUM");
         sNames.put(k++, "A_AVG");
         sNames.put(k++, "A_LEN");
+        sNames.put(k++, "A_SPLINE");
 
         sNames.put(k++, "a[0]");
         sNames.put(k++, "a[1]");
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ArrayAccess.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ArrayAccess.java
index eb5e482..182d36a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ArrayAccess.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ArrayAccess.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.utilities;
 
+import android.annotation.Nullable;
+
 /**
  * Support a standardized interface to commands that contain arrays All commands that implement
  * array access will be collected in a map in the state TODO refactor to DataAccess,
@@ -27,6 +29,7 @@
         return 0;
     }
 
+    @Nullable
     float[] getFloats();
 
     int getLength();
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java
index 0128253..4f12872 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.utilities;
 
+import android.annotation.Nullable;
+
 /**
  * interface to allow expressions to access collections Todo define a convention for when access is
  * unavailable
@@ -22,6 +24,7 @@
 public interface CollectionsAccess {
     float getFloatValue(int id, int index);
 
+    @Nullable
     float[] getFloats(int id);
 
     int getListLength(int id);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java
index 24f17d7..07a3d84 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java
@@ -15,18 +15,20 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.utilities;
 
-public class DataMap {
-    public String[] mNames;
-    public int[] mIds;
-    public byte[] mTypes;
+import android.annotation.NonNull;
 
-    public DataMap(String[] names, byte[] types, int[] ids) {
+public class DataMap {
+    @NonNull public final String[] mNames;
+    @NonNull public final int[] mIds;
+    @NonNull public final byte[] mTypes;
+
+    public DataMap(@NonNull String[] names, @NonNull byte[] types, @NonNull int[] ids) {
         mNames = names;
         mTypes = types;
         mIds = ids;
     }
 
-    public int getPos(String str) {
+    public int getPos(@NonNull String str) {
         for (int i = 0; i < mNames.length; i++) {
             String name = mNames[i];
             if (str.equals(name)) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java
index e74b335..98ee91b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java
@@ -17,6 +17,8 @@
 
 import android.annotation.NonNull;
 
+import com.android.internal.widget.remotecompose.core.operations.Utils;
+
 /** Implement the scaling logic for Compose Image or ImageView */
 public class ImageScaling {
 
@@ -109,7 +111,7 @@
         String s = str;
         s += str(left) + ", " + str(top) + ", " + str(right) + ", " + str(bottom) + ", ";
         s += " [" + str(right - left) + " x " + str(bottom - top) + "]";
-        System.out.println(s);
+        Utils.log(s);
     }
 
     /** This adjust destnation on the DrawBitMapInt to support all contentScale types */
@@ -128,7 +130,7 @@
             print("test rc ", mSrcLeft, mSrcTop, mSrcRight, mSrcBottom);
             print("test dst ", mDstLeft, mDstTop, mDstRight, mDstBottom);
         }
-
+        if (sh == 0 || sw == 0) return;
         switch (mScaleType) {
             case SCALE_NONE:
                 dh = sh;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
index 749c0fe..b9aa881 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
@@ -15,6 +15,7 @@
  */
 package com.android.internal.widget.remotecompose.core.operations.utilities;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 
 import java.util.ArrayList;
@@ -45,7 +46,7 @@
     }
 
     @Nullable
-    public T put(int key, T value) {
+    public T put(int key, @NonNull T value) {
         if (key == NOT_PRESENT) throw new IllegalArgumentException("Key cannot be NOT_PRESENT");
         if (mSize > mKeys.length * LOAD_FACTOR) {
             resize();
@@ -66,7 +67,7 @@
     }
 
     @Nullable
-    private T insert(int key, T value) {
+    private T insert(int key, @NonNull T value) {
         int index = hash(key) % mKeys.length;
         while (mKeys[index] != NOT_PRESENT && mKeys[index] != key) {
             index = (index + 1) % mKeys.length;
@@ -116,6 +117,7 @@
         }
     }
 
+    @Nullable
     public T remove(int key) {
         int index = hash(key) % mKeys.length;
         int initialIndex = index;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
index 8905431..f73ab39 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
@@ -59,9 +59,9 @@
     public static final int I_VAR1 = OFFSET + 24;
     public static final int I_VAR2 = OFFSET + 25;
 
-    int[] mStack;
+    @NonNull int[] mStack = new int[0];
     @NonNull int[] mLocalStack = new int[128];
-    int[] mVar;
+    @NonNull int[] mVar = new int[0];
 
     interface Op {
         int eval(int sp);
@@ -75,7 +75,7 @@
      * @param var variables if the expression is a function
      * @return return the results of evaluating the expression
      */
-    public int eval(int mask, int[] exp, int... var) {
+    public int eval(int mask, @NonNull int[] exp, @NonNull int... var) {
         mStack = exp;
         mVar = var;
         int sp = -1;
@@ -99,7 +99,7 @@
      * @param var variables if the expression is a function
      * @return return the results of evaluating the expression
      */
-    public int eval(int mask, @NonNull int[] exp, int len, int... var) {
+    public int eval(int mask, @NonNull int[] exp, int len, @NonNull int... var) {
         System.arraycopy(exp, 0, mLocalStack, 0, len);
         mStack = mLocalStack;
         mVar = var;
@@ -123,17 +123,15 @@
      * @param var variables if the expression is a function
      * @return return the results of evaluating the expression
      */
-    public int evalDB(int opMask, @NonNull int[] exp, int... var) {
+    public int evalDB(int opMask, @NonNull int[] exp, @NonNull int... var) {
         mStack = exp;
         mVar = var;
         int sp = -1;
         for (int i = 0; i < exp.length; i++) {
             int v = mStack[i];
             if (((1 << i) & opMask) != 0) {
-                System.out.print(" " + sNames.get((v - OFFSET)));
                 sp = mOps[v - OFFSET].eval(sp);
             } else {
-                System.out.print(" " + v);
                 mStack[++sp] = v;
             }
         }
@@ -199,7 +197,7 @@
                     return sp - 1;
                 };
         Op mCOPY_SIGN =
-                (sp) -> { // COPY_SIGN
+                (sp) -> { // COPY_SIGN copy the sign via bit manipulation
                     mStack[sp - 1] = (mStack[sp - 1] ^ (mStack[sp] >> 31)) - (mStack[sp] >> 31);
                     return sp - 1;
                 };
@@ -239,12 +237,12 @@
                     return sp;
                 };
         Op mSIGN =
-                (sp) -> { // SIGN
+                (sp) -> { // SIGN x<0 = -1,x==0 =  0 , x>0 = 1
                     mStack[sp] = (mStack[sp] >> 31) | (-mStack[sp] >>> 31);
                     return sp;
                 };
         Op mCLAMP =
-                (sp) -> { // CLAMP
+                (sp) -> { // CLAMP(min,max, val)
                     mStack[sp - 2] = Math.min(Math.max(mStack[sp - 2], mStack[sp]), mStack[sp - 1]);
                     return sp - 2;
                 };
@@ -360,7 +358,7 @@
      * @return
      */
     @NonNull
-    public static String toString(int opMask, @NonNull int[] exp, String[] labels) {
+    public static String toString(int opMask, @NonNull int[] exp, @NonNull String[] labels) {
         StringBuilder s = new StringBuilder();
         for (int i = 0; i < exp.length; i++) {
             int v = exp[i];
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java
index ebb22b6..465c95d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java
@@ -57,13 +57,17 @@
         mEasingCurve = new CubicEasing(mType);
     }
 
-    public FloatAnimation(float... description) {
+    public FloatAnimation(@NonNull float... description) {
         mType = CUBIC_STANDARD;
         setAnimationDescription(description);
     }
 
     public FloatAnimation(
-            int type, float duration, float[] description, float initialValue, float wrap) {
+            int type,
+            float duration,
+            @Nullable float[] description,
+            float initialValue,
+            float wrap) {
         mType = CUBIC_STANDARD;
         setAnimationDescription(packToFloatArray(duration, type, description, initialValue, wrap));
     }
@@ -77,7 +81,7 @@
      * @param initialValue
      * @return
      */
-    public static float[] packToFloatArray(
+    public static @NonNull float[] packToFloatArray(
             float duration, int type, @Nullable float[] spec, float initialValue, float wrap) {
         int count = 0;
 
@@ -221,7 +225,7 @@
      *
      * @param description
      */
-    public void setAnimationDescription(float[] description) {
+    public void setAnimationDescription(@NonNull float[] description) {
         mSpec = description;
         mDuration = (mSpec.length == 0) ? 1 : mSpec[0];
         int len = 0;
@@ -242,7 +246,7 @@
         create(mType, description, 2, len);
     }
 
-    private void create(int type, float[] params, int offset, int len) {
+    private void create(int type, @Nullable float[] params, int offset, int len) {
         switch (type) {
             case CUBIC_STANDARD:
             case CUBIC_ACCELERATE:
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
index 90b65bf..06969cc 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
@@ -19,7 +19,7 @@
 
 /** Provides and interface to create easing functions */
 public class GeneralEasing extends Easing {
-    float[] mEasingData = new float[0];
+    @NonNull float[] mEasingData = new float[0];
     @NonNull Easing mEasingCurve = new CubicEasing(CUBIC_STANDARD);
 
     /**
@@ -27,12 +27,12 @@
      *
      * @param data
      */
-    public void setCurveSpecification(float[] data) {
+    public void setCurveSpecification(@NonNull float[] data) {
         mEasingData = data;
         createEngine();
     }
 
-    public float[] getCurveSpecification() {
+    public @NonNull float[] getCurveSpecification() {
         return mEasingData;
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicCurveFit.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicCurveFit.java
index f540e70..f4579a2 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicCurveFit.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicCurveFit.java
@@ -22,11 +22,11 @@
 /** This performs a spline interpolation in multiple dimensions */
 public class MonotonicCurveFit {
     private static final String TAG = "MonotonicCurveFit";
-    private double[] mT;
-    private double[][] mY;
-    private double[][] mTangent;
+    @NonNull private final double[] mT;
+    @NonNull private final double[][] mY;
+    @NonNull private final double[][] mTangent;
     private boolean mExtrapolate = true;
-    double[] mSlopeTemp;
+    @NonNull final double[] mSlopeTemp;
 
     /**
      * create a collection of curves
@@ -81,7 +81,7 @@
      * @param t
      * @param v
      */
-    public void getPos(double t, double[] v) {
+    public void getPos(double t, @NonNull double[] v) {
         final int n = mT.length;
         final int dim = mY[0].length;
         if (mExtrapolate) {
@@ -141,7 +141,7 @@
      * @param t
      * @param v
      */
-    public void getPos(double t, float[] v) {
+    public void getPos(double t, @NonNull float[] v) {
         final int n = mT.length;
         final int dim = mY[0].length;
         if (mExtrapolate) {
@@ -243,7 +243,7 @@
      * @param t
      * @param v
      */
-    public void getSlope(double t, double[] v) {
+    public void getSlope(double t, @NonNull double[] v) {
         final int n = mT.length;
         int dim = mY[0].length;
         if (t <= mT[0]) {
@@ -297,7 +297,7 @@
         return 0; // should never reach here
     }
 
-    public double[] getTimePoints() {
+    public @NonNull double[] getTimePoints() {
         return mT;
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicSpline.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicSpline.java
new file mode 100644
index 0000000..23a6643
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/MonotonicSpline.java
@@ -0,0 +1,203 @@
+/*
+ * 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.widget.remotecompose.core.operations.utilities.easing;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** This performs a spline interpolation in multiple dimensions */
+public class MonotonicSpline {
+    private static final String TAG = "MonotonicCurveFit";
+    private float[] mT;
+    private float[] mY;
+    private float[] mTangent;
+    private boolean mExtrapolate = true;
+    float[] mSlopeTemp;
+
+    /**
+     * create a collection of curves
+     *
+     * @param time the point along the curve
+     * @param y the parameter at those points
+     */
+    public MonotonicSpline(@Nullable float[] time, @NonNull float[] y) {
+        if (time == null) { // if time  is null assume even 0 to 1;
+            time = new float[y.length];
+            for (int i = 0; i < time.length; i++) {
+                time[i] = i / (float) (time.length - 1);
+            }
+        }
+        mT = time;
+        mY = y;
+        final int n = time.length;
+        final int dim = 1;
+        mSlopeTemp = new float[dim];
+        float[] slope = new float[n - 1]; // could optimize this out
+        float[] tangent = new float[n];
+        for (int i = 0; i < n - 1; i++) {
+            float dt = time[i + 1] - time[i];
+            slope[i] = (y[i + 1] - y[i]) / dt;
+            if (i == 0) {
+                tangent[i] = slope[i];
+            } else {
+                tangent[i] = (slope[i - 1] + slope[i]) * 0.5f;
+            }
+        }
+        tangent[n - 1] = slope[n - 2];
+
+        for (int i = 0; i < n - 1; i++) {
+            if (slope[i] == 0.) {
+                tangent[i] = 0f;
+                tangent[i + 1] = 0f;
+            } else {
+                float a = tangent[i] / slope[i];
+                float b = tangent[i + 1] / slope[i];
+                float h = (float) Math.hypot(a, b);
+                if (h > 9.0) {
+                    float t = 3f / h;
+                    tangent[i] = t * a * slope[i];
+                    tangent[i + 1] = t * b * slope[i];
+                }
+            }
+        }
+        mTangent = tangent;
+    }
+
+    public float[] getArray() {
+        return mY;
+    }
+
+    /**
+     * Get the position of all curves at time t
+     *
+     * @param t
+     * @return position at t
+     */
+    public float getPos(float t) {
+        final int n = mT.length;
+        float v;
+        if (mExtrapolate) {
+            if (t <= mT[0]) {
+                float slopeTemp = getSlope(mT[0]);
+                v = mY[0] + (t - mT[0]) * slopeTemp;
+
+                return v;
+            }
+            if (t >= mT[n - 1]) {
+                float slopeTemp = getSlope(mT[n - 1]);
+                v = mY[n - 1] + (t - mT[n - 1]) * slopeTemp;
+
+                return v;
+            }
+        } else {
+            if (t <= mT[0]) {
+                v = mY[0];
+
+                return v;
+            }
+            if (t >= mT[n - 1]) {
+                v = mY[n - 1];
+
+                return v;
+            }
+        }
+
+        for (int i = 0; i < n - 1; i++) {
+            if (t == mT[i]) {
+
+                v = mY[i];
+            }
+            if (t < mT[i + 1]) {
+                float h = mT[i + 1] - mT[i];
+                float x = (t - mT[i]) / h;
+
+                float y1 = mY[i];
+                float y2 = mY[i + 1];
+                float t1 = mTangent[i];
+                float t2 = mTangent[i + 1];
+                v = interpolate(h, x, y1, y2, t1, t2);
+
+                return v;
+            }
+        }
+        return 0f;
+    }
+
+    /**
+     * Get the slope of the curve at position t
+     *
+     * @param t
+     * @return slope at t
+     */
+    public float getSlope(float t) {
+        final int n = mT.length;
+        float v = 0;
+
+        if (t <= mT[0]) {
+            t = mT[0];
+        } else if (t >= mT[n - 1]) {
+            t = mT[n - 1];
+        }
+
+        for (int i = 0; i < n - 1; i++) {
+            if (t <= mT[i + 1]) {
+                float h = mT[i + 1] - mT[i];
+                float x = (t - mT[i]) / h;
+                float y1 = mY[i];
+                float y2 = mY[i + 1];
+                float t1 = mTangent[i];
+                float t2 = mTangent[i + 1];
+                v = diff(h, x, y1, y2, t1, t2) / h;
+            }
+            break;
+        }
+        return v;
+    }
+
+    public float[] getTimePoints() {
+        return mT;
+    }
+
+    /** Cubic Hermite spline */
+    private static float interpolate(float h, float x, float y1, float y2, float t1, float t2) {
+        float x2 = x * x;
+        float x3 = x2 * x;
+        return -2 * x3 * y2
+                + 3 * x2 * y2
+                + 2 * x3 * y1
+                - 3 * x2 * y1
+                + y1
+                + h * t2 * x3
+                + h * t1 * x3
+                - h * t2 * x2
+                - 2 * h * t1 * x2
+                + h * t1 * x;
+    }
+
+    /** Cubic Hermite spline slope differentiated */
+    private static float diff(float h, float x, float y1, float y2, float t1, float t2) {
+        float x2 = x * x;
+        return -6 * x2 * y2
+                + 6 * x * y2
+                + 6 * x2 * y1
+                - 6 * x * y1
+                + 3 * h * t2 * x2
+                + 3 * h * t1 * x2
+                - 2 * h * t2 * x
+                - 4 * h * t1 * x
+                + h * t1;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/SpringStopEngine.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/SpringStopEngine.java
new file mode 100644
index 0000000..03e4503
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/SpringStopEngine.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.internal.widget.remotecompose.core.operations.utilities.easing;
+
+/**
+ * This contains the class to provide the logic for an animation to come to a stop using a spring
+ * model. String debug(String desc, float time); float getVelocity(float time); float
+ * getInterpolation(float time); float getVelocity(); boolean isStopped();
+ */
+public class SpringStopEngine {
+    double mDamping = 0.5f;
+
+    @SuppressWarnings("unused")
+    private static final double UNSET = Double.MAX_VALUE;
+
+    @SuppressWarnings("unused")
+    private boolean mInitialized = false;
+
+    private double mStiffness;
+    private double mTargetPos;
+
+    @SuppressWarnings("unused")
+    private double mLastVelocity;
+
+    private float mLastTime;
+    private float mPos;
+    private float mV;
+    private float mMass;
+    private float mStopThreshold;
+    private int mBoundaryMode = 0;
+
+    public String debug(String desc, float time) {
+        return null;
+    }
+
+    void log(String str) {
+        StackTraceElement s = new Throwable().getStackTrace()[1];
+        String line =
+                ".(" + s.getFileName() + ":" + s.getLineNumber() + ") " + s.getMethodName() + "() ";
+        System.out.println(line + str);
+    }
+
+    public SpringStopEngine() {}
+
+    public float getTargetValue() {
+        return (float) mTargetPos;
+    }
+
+    public void setInitialValue(float v) {
+        mPos = v;
+    }
+
+    public void setTargetValue(float v) {
+        mTargetPos = v;
+    }
+
+    public SpringStopEngine(float[] parameters) {
+        if (parameters[0] != 0) {
+            throw new RuntimeException(" parameter[0] should be 0");
+        }
+
+        springParameters(
+                1,
+                parameters[1],
+                parameters[2],
+                parameters[3],
+                Float.floatToRawIntBits(parameters[4]));
+    }
+
+    /**
+     * Config the spring starting conditions
+     *
+     * @param currentPos
+     * @param target
+     * @param currentVelocity
+     */
+    public void springStart(float currentPos, float target, float currentVelocity) {
+        mTargetPos = target;
+        mInitialized = false;
+        mPos = currentPos;
+        mLastVelocity = currentVelocity;
+        mLastTime = 0;
+    }
+
+    /**
+     * Config the spring parameters
+     *
+     * @param mass The mass of the spring
+     * @param stiffness The stiffness of the spring
+     * @param damping The dampening factor
+     * @param stopThreshold how low energy must you be to stop
+     * @param boundaryMode The boundary behaviour
+     */
+    public void springParameters(
+            float mass, float stiffness, float damping, float stopThreshold, int boundaryMode) {
+        mDamping = damping;
+        mInitialized = false;
+        mStiffness = stiffness;
+        mMass = mass;
+        mStopThreshold = stopThreshold;
+        mBoundaryMode = boundaryMode;
+        mLastTime = 0;
+    }
+
+    public float getVelocity(float time) {
+        return (float) mV;
+    }
+
+    public float get(float time) {
+        compute(time - mLastTime);
+        mLastTime = time;
+        if (isStopped()) {
+            mPos = (float) mTargetPos;
+        }
+        return (float) mPos;
+    }
+
+    public float getAcceleration() {
+        double k = mStiffness;
+        double c = mDamping;
+        double x = (mPos - mTargetPos);
+        return (float) (-k * x - c * mV) / mMass;
+    }
+
+    public float getVelocity() {
+        return 0;
+    }
+
+    public boolean isStopped() {
+        double x = (mPos - mTargetPos);
+        double k = mStiffness;
+        double v = mV;
+        double m = mMass;
+        double energy = v * v * m + k * x * x;
+        double max_def = Math.sqrt(energy / k);
+        return max_def <= mStopThreshold;
+    }
+
+    private void compute(double dt) {
+        if (dt <= 0) {
+            // Nothing to compute if there's no time difference
+            return;
+        }
+
+        double k = mStiffness;
+        double c = mDamping;
+        // Estimate how many time we should over sample based on the frequency and current sampling
+        int overSample = (int) (1 + 9 / (Math.sqrt(mStiffness / mMass) * dt * 4));
+        dt /= overSample;
+
+        for (int i = 0; i < overSample; i++) {
+            double x = (mPos - mTargetPos);
+            double a = (-k * x - c * mV) / mMass;
+            // This refinement of a simple coding of the acceleration increases accuracy
+            double avgV = mV + a * dt / 2; // pass 1 calculate the average velocity
+            double avgX = mPos + dt * avgV / 2 - mTargetPos; // pass 1 calculate the average pos
+            a = (-avgX * k - avgV * c) / mMass; //  calculate acceleration over that average pos
+
+            double dv = a * dt; //  calculate change in velocity
+            avgV = mV + dv / 2; //  average  velocity is current + half change
+            mV += (float) dv;
+            mPos += (float) (avgV * dt);
+            if (mBoundaryMode > 0) {
+                if (mPos < 0 && ((mBoundaryMode & 1) == 1)) {
+                    mPos = -mPos;
+                    mV = -mV;
+                }
+                if (mPos > 1 && ((mBoundaryMode & 2) == 2)) {
+                    mPos = 2 - mPos;
+                    mV = -mV;
+                }
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/StepCurve.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/StepCurve.java
index c7be3ca..b1eb804 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/StepCurve.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/StepCurve.java
@@ -24,14 +24,14 @@
  */
 public class StepCurve extends Easing {
     //    private static final boolean DEBUG = false;
-    MonotonicCurveFit mCurveFit;
+    @NonNull private final MonotonicCurveFit mCurveFit;
 
-    public StepCurve(float[] params, int offset, int len) {
+    public StepCurve(@NonNull float[] params, int offset, int len) {
         mCurveFit = genSpline(params, offset, len);
     }
 
     @NonNull
-    private static MonotonicCurveFit genSpline(float[] values, int off, int arrayLen) {
+    private static MonotonicCurveFit genSpline(@NonNull float[] values, int off, int arrayLen) {
         int length = arrayLen * 3 - 2;
         int len = arrayLen - 1;
         double gap = 1.0 / len;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/touch/VelocityEasing.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/touch/VelocityEasing.java
index 3e24372..7e02bc9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/touch/VelocityEasing.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/touch/VelocityEasing.java
@@ -212,7 +212,6 @@
                 mStage[1].setUp(peak_v, d1, t1, 0f, destination, t2 + t1);
                 mDuration = t2 + t1;
                 if (mDuration > maxTime) {
-                    System.out.println(" fail ");
                     return false;
                 }
             }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java
index 3fba8ac..4af79f3 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java
@@ -54,11 +54,11 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {}
+    public void apply(@NonNull RemoteContext context) {}
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
index 79f2a8d..613e732 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
@@ -50,7 +50,7 @@
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
index 01672b4..745caa3 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
@@ -54,13 +54,13 @@
     }
 
     @Override
-    public void apply(RemoteContext context) {
+    public void apply(@NonNull RemoteContext context) {
         context.putObject(mId, this);
     }
 
     @NonNull
     @Override
-    public String deepToString(String indent) {
+    public String deepToString(@NonNull String indent) {
         return toString();
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
index aaee9c5..2a3f3be 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
@@ -141,4 +141,9 @@
         }
         return mDocument.getStats();
     }
+
+    public int hasSensorListeners(int[] ids) {
+
+        return 0;
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
index cc74b11..648f7bf 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -15,9 +15,14 @@
  */
 package com.android.internal.widget.remotecompose.player;
 
+import android.app.Application;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.TypedValue;
@@ -28,6 +33,7 @@
 import android.widget.ScrollView;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas;
@@ -81,6 +87,7 @@
             mInner.setDocument(null);
         }
         mapColors();
+        setupSensors();
         mInner.setHapticEngine(
                 new CoreDocument.HapticEngine() {
 
@@ -543,4 +550,113 @@
     private void provideHapticFeedback(int type) {
         performHapticFeedback(sHapticTable[type % sHapticTable.length]);
     }
+
+    SensorManager mSensorManager;
+    Sensor mAcc = null, mGyro = null, mMag = null, mLight = null;
+    SensorEventListener mListener;
+
+    private void setupSensors() {
+
+        int minId = RemoteContext.ID_ACCELERATION_X;
+        int maxId = RemoteContext.ID_LIGHT;
+        int[] ids = new int[1 + maxId - minId];
+
+        int count = mInner.hasSensorListeners(ids);
+        mAcc = null;
+        mGyro = null;
+        mMag = null;
+        mLight = null;
+        if (count > 0) {
+            Application app = (Application) getContext().getApplicationContext();
+
+            mSensorManager = (SensorManager) app.getSystemService(Context.SENSOR_SERVICE);
+            for (int i = 0; i < count; i++) {
+                switch (ids[i]) {
+                    case RemoteContext.ID_ACCELERATION_X:
+                    case RemoteContext.ID_ACCELERATION_Y:
+                    case RemoteContext.ID_ACCELERATION_Z:
+                        if (mAcc == null) {
+                            mAcc = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+                        }
+                        break;
+                    case RemoteContext.ID_GYRO_ROT_X:
+                    case RemoteContext.ID_GYRO_ROT_Y:
+                    case RemoteContext.ID_GYRO_ROT_Z:
+                        if (mGyro == null) {
+                            mGyro = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+                        }
+                        break;
+                    case RemoteContext.ID_MAGNETIC_X:
+                    case RemoteContext.ID_MAGNETIC_Y:
+                    case RemoteContext.ID_MAGNETIC_Z:
+                        if (mMag == null) {
+                            mMag = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+                        }
+                        break;
+                    case RemoteContext.ID_LIGHT:
+                        if (mLight == null) {
+                            mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
+                        }
+                }
+            }
+        }
+        registerListener();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        unregisterListener();
+    }
+
+    public void registerListener() {
+        Sensor[] s = {mAcc, mGyro, mMag, mLight};
+        if (mListener != null) {
+            unregisterListener();
+        }
+        SensorEventListener listener =
+                new SensorEventListener() {
+                    @Override
+                    public void onSensorChanged(SensorEvent event) {
+                        if (event.sensor == mAcc) {
+                            mInner.setExternalFloat(
+                                    RemoteContext.ID_ACCELERATION_X, event.values[0]);
+                            mInner.setExternalFloat(
+                                    RemoteContext.ID_ACCELERATION_Y, event.values[1]);
+                            mInner.setExternalFloat(
+                                    RemoteContext.ID_ACCELERATION_Z, event.values[2]);
+                        } else if (event.sensor == mGyro) {
+                            mInner.setExternalFloat(RemoteContext.ID_GYRO_ROT_X, event.values[0]);
+                            mInner.setExternalFloat(RemoteContext.ID_GYRO_ROT_Y, event.values[1]);
+                            mInner.setExternalFloat(RemoteContext.ID_GYRO_ROT_Z, event.values[2]);
+                        } else if (event.sensor == mMag) {
+                            mInner.setExternalFloat(RemoteContext.ID_MAGNETIC_X, event.values[0]);
+                            mInner.setExternalFloat(RemoteContext.ID_MAGNETIC_Y, event.values[1]);
+                            mInner.setExternalFloat(RemoteContext.ID_MAGNETIC_Z, event.values[2]);
+                        } else if (event.sensor == mLight) {
+                            mInner.setExternalFloat(RemoteContext.ID_LIGHT, event.values[0]);
+                        }
+                    }
+
+                    @Override
+                    public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+                };
+
+        Sensor[] sensors = {mAcc, mGyro, mMag, mLight};
+        for (int i = 0; i < sensors.length; i++) {
+            Sensor sensor = sensors[i];
+            if (sensor != null) {
+                mListener = listener;
+                mSensorManager.registerListener(
+                        mListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
+            }
+        }
+    }
+
+    public void unregisterListener() {
+        if (mListener != null && mSensorManager != null) {
+            mSensorManager.unregisterListener(mListener);
+        }
+        mListener = null;
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index 0b650a9..3c91cff 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -15,6 +15,7 @@
  */
 package com.android.internal.widget.remotecompose.player.platform;
 
+import android.annotation.NonNull;
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
@@ -247,7 +248,7 @@
     }
 
     @Override
-    public void getTextBounds(int textId, int start, int end, int flags, float[] bounds) {
+    public void getTextBounds(int textId, int start, int end, int flags, @NonNull float[] bounds) {
         String str = getText(textId);
         if (end == -1) {
             end = str.length();
@@ -420,7 +421,7 @@
      * @param paintData the list change to the paint
      */
     @Override
-    public void applyPaint(PaintBundle paintData) {
+    public void applyPaint(@NonNull PaintBundle paintData) {
         paintData.applyPaintChange(
                 (PaintContext) this,
                 new PaintChanges() {
@@ -576,8 +577,8 @@
 
                     @Override
                     public void setLinearGradient(
-                            int[] colors,
-                            float[] stops,
+                            @NonNull int[] colors,
+                            @NonNull float[] stops,
                             float startX,
                             float startY,
                             float endX,
@@ -596,8 +597,8 @@
 
                     @Override
                     public void setRadialGradient(
-                            int[] colors,
-                            float[] stops,
+                            @NonNull int[] colors,
+                            @NonNull float[] stops,
                             float centerX,
                             float centerY,
                             float radius,
@@ -614,7 +615,10 @@
 
                     @Override
                     public void setSweepGradient(
-                            int[] colors, float[] stops, float centerX, float centerY) {
+                            @NonNull int[] colors,
+                            @NonNull float[] stops,
+                            float centerX,
+                            float centerY) {
                         mPaint.setShader(new SweepGradient(centerX, centerY, colors, stops));
                     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPlatformServices.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPlatformServices.java
index f28e85a..ba8d83b 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPlatformServices.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPlatformServices.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.player.platform;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.graphics.Bitmap;
 import android.graphics.Path;
 import android.graphics.PathIterator;
@@ -31,7 +33,7 @@
     private static final String LOG_TAG = "RemoteCompose";
 
     @Override
-    public byte[] imageToByteArray(Object image) {
+    public byte[] imageToByteArray(@NonNull Object image) {
         if (image instanceof Bitmap) {
             // let's create a bitmap
             ByteArrayOutputStream byteArrayBitmapStream = new ByteArrayOutputStream();
@@ -42,7 +44,7 @@
     }
 
     @Override
-    public int getImageWidth(Object image) {
+    public int getImageWidth(@NonNull Object image) {
         if (image instanceof Bitmap) {
             return ((Bitmap) image).getWidth();
         }
@@ -50,7 +52,7 @@
     }
 
     @Override
-    public int getImageHeight(Object image) {
+    public int getImageHeight(@NonNull Object image) {
         if (image instanceof Bitmap) {
             return ((Bitmap) image).getHeight();
         }
@@ -58,7 +60,8 @@
     }
 
     @Override
-    public float[] pathToFloatArray(Object path) {
+    @Nullable
+    public float[] pathToFloatArray(@NonNull Object path) {
         //        if (path is RemotePath) {
         //            return path.createFloatArray()
         //        }
@@ -88,7 +91,7 @@
         }
     }
 
-    private float[] androidPathToFloatArray(Path path) {
+    private @NonNull float[] androidPathToFloatArray(@NonNull Path path) {
         PathIterator i = path.getPathIterator();
         int estimatedSize = 0;
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
index 7a7edba..77c2514 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.player.platform;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
@@ -22,11 +24,15 @@
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.TouchListener;
 import com.android.internal.widget.remotecompose.core.VariableSupport;
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
 import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
 import com.android.internal.widget.remotecompose.core.operations.ShaderData;
 import com.android.internal.widget.remotecompose.core.operations.utilities.ArrayAccess;
 import com.android.internal.widget.remotecompose.core.operations.utilities.DataMap;
 
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.HashMap;
 
 /**
@@ -53,7 +59,7 @@
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
     @Override
-    public void loadPathData(int instanceId, float[] floatPath) {
+    public void loadPathData(int instanceId, @NonNull float[] floatPath) {
         if (!mRemoteComposeState.containsId(instanceId)) {
             mRemoteComposeState.cacheData(instanceId, floatPath);
         }
@@ -74,12 +80,12 @@
     HashMap<String, VarName> mVarNameHashMap = new HashMap<>();
 
     @Override
-    public void loadVariableName(String varName, int varId, int varType) {
+    public void loadVariableName(@NonNull String varName, int varId, int varType) {
         mVarNameHashMap.put(varName, new VarName(varName, varId, varType));
     }
 
     @Override
-    public void setNamedStringOverride(String stringName, String value) {
+    public void setNamedStringOverride(@NonNull String stringName, @NonNull String value) {
         if (mVarNameHashMap.get(stringName) != null) {
             int id = mVarNameHashMap.get(stringName).mId;
             overrideText(id, value);
@@ -87,7 +93,7 @@
     }
 
     @Override
-    public void clearNamedStringOverride(String stringName) {
+    public void clearNamedStringOverride(@NonNull String stringName) {
         if (mVarNameHashMap.get(stringName) != null) {
             int id = mVarNameHashMap.get(stringName).mId;
             clearDataOverride(id);
@@ -96,7 +102,7 @@
     }
 
     @Override
-    public void setNamedIntegerOverride(String stringName, int value) {
+    public void setNamedIntegerOverride(@NonNull String stringName, int value) {
         if (mVarNameHashMap.get(stringName) != null) {
             int id = mVarNameHashMap.get(stringName).mId;
             overrideInt(id, value);
@@ -104,7 +110,7 @@
     }
 
     @Override
-    public void clearNamedIntegerOverride(String integerName) {
+    public void clearNamedIntegerOverride(@NonNull String integerName) {
         if (mVarNameHashMap.get(integerName) != null) {
             int id = mVarNameHashMap.get(integerName).mId;
             clearIntegerOverride(id);
@@ -118,18 +124,18 @@
      * @param colorName name of color
      * @param color
      */
-    public void setNamedColorOverride(String colorName, int color) {
+    public void setNamedColorOverride(@NonNull String colorName, int color) {
         int id = mVarNameHashMap.get(colorName).mId;
         mRemoteComposeState.overrideColor(id, color);
     }
 
     @Override
-    public void addCollection(int id, ArrayAccess collection) {
+    public void addCollection(int id, @NonNull ArrayAccess collection) {
         mRemoteComposeState.addCollection(id, collection);
     }
 
     @Override
-    public void putDataMap(int id, DataMap map) {
+    public void putDataMap(int id, @NonNull DataMap map) {
         mRemoteComposeState.putDataMap(id, map);
     }
 
@@ -139,7 +145,7 @@
     }
 
     @Override
-    public void runAction(int id, String metadata) {
+    public void runAction(int id, @NonNull String metadata) {
         mDocument.performClick(id);
     }
 
@@ -152,21 +158,66 @@
     /**
      * Decode a byte array into an image and cache it using the given imageId
      *
-     * @param width with of image to be loaded
+     * @param encoding how the data is encoded 0 = png, 1 = raw, 2 = url
+     * @param type the type of the data 0 = RGBA 8888, 1 = 888, 2 = 8 gray
+     * @param width with of image to be loaded largest dimension is 32767
      * @param height height of image to be loaded
      * @param bitmap a byte array containing the image information
      * @oaram imageId the id of the image
      */
     @Override
-    public void loadBitmap(int imageId, int width, int height, byte[] bitmap) {
+    public void loadBitmap(
+            int imageId, short encoding, short type, int width, int height, @NonNull byte[] data) {
         if (!mRemoteComposeState.containsId(imageId)) {
-            Bitmap image = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length);
+            Bitmap image = null;
+            switch (encoding) {
+                case BitmapData.ENCODING_INLINE:
+                    switch (type) {
+                        case BitmapData.TYPE_PNG_8888:
+                            image = BitmapFactory.decodeByteArray(data, 0, data.length);
+                            break;
+                        case BitmapData.TYPE_RAW8888:
+                            image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+                            int[] idata = new int[data.length / 4];
+                            for (int i = 0; i < idata.length; i++) {
+                                int p = i * 4;
+                                idata[i] =
+                                        (data[p] << 24)
+                                                | (data[p + 1] << 16)
+                                                | (data[p + 2] << 8)
+                                                | data[p + 3];
+                            }
+                            image.setPixels(idata, 0, width, 0, 0, width, height);
+                            break;
+                        case BitmapData.TYPE_RAW8:
+                            image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+                            int[] bdata = new int[data.length / 4];
+                            for (int i = 0; i < bdata.length; i++) {
+
+                                bdata[i] = 0x1010101 * data[i];
+                            }
+                            image.setPixels(bdata, 0, width, 0, 0, width, height);
+                            break;
+                    }
+                    break;
+                case BitmapData.ENCODING_FILE:
+                    image = BitmapFactory.decodeFile(new String(data));
+                    break;
+                case BitmapData.ENCODING_URL:
+                    try {
+                        image = BitmapFactory.decodeStream(new URL(new String(data)).openStream());
+                    } catch (MalformedURLException e) {
+                        throw new RuntimeException(e);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+            }
             mRemoteComposeState.cacheData(imageId, image);
         }
     }
 
     @Override
-    public void loadText(int id, String text) {
+    public void loadText(int id, @NonNull String text) {
         if (!mRemoteComposeState.containsId(id)) {
             mRemoteComposeState.cacheData(id, text);
         } else {
@@ -225,12 +276,12 @@
     }
 
     @Override
-    public void loadAnimatedFloat(int id, FloatExpression animatedFloat) {
+    public void loadAnimatedFloat(int id, @NonNull FloatExpression animatedFloat) {
         mRemoteComposeState.cacheData(id, animatedFloat);
     }
 
     @Override
-    public void loadShader(int id, ShaderData value) {
+    public void loadShader(int id, @NonNull ShaderData value) {
         mRemoteComposeState.cacheData(id, value);
     }
 
@@ -240,7 +291,7 @@
     }
 
     @Override
-    public void putObject(int id, Object value) {
+    public void putObject(int id, @NonNull Object value) {
         mRemoteComposeState.updateObject(id, value);
     }
 
@@ -260,7 +311,7 @@
     }
 
     @Override
-    public void listensTo(int id, VariableSupport variableSupport) {
+    public void listensTo(int id, @NonNull VariableSupport variableSupport) {
         mRemoteComposeState.listenToVar(id, variableSupport);
     }
 
@@ -270,6 +321,7 @@
     }
 
     @Override
+    @Nullable
     public ShaderData getShader(int id) {
         return (ShaderData) mRemoteComposeState.getFromId(id);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
index fdd9aad..41ed017 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
@@ -15,6 +15,7 @@
  */
 package com.android.internal.widget.remotecompose.player.platform;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
@@ -23,13 +24,17 @@
 /** Implementation for the click handling */
 class ClickAreaView extends View {
     private int mId;
-    private String mMetadata;
+    private final String mMetadata;
     Paint mPaint = new Paint();
 
     private boolean mDebug;
 
     ClickAreaView(
-            Context context, boolean debug, int id, String contentDescription, String metadata) {
+            Context context,
+            boolean debug,
+            int id,
+            @Nullable String contentDescription,
+            String metadata) {
         super(context);
         this.mId = id;
         this.mMetadata = metadata;
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
index b54ed8a..8f55f8a 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -26,6 +26,7 @@
 import android.widget.FrameLayout;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
@@ -194,6 +195,20 @@
         }
     }
 
+    public int hasSensorListeners(int[] ids) {
+        int count = 0;
+        for (int id = RemoteContext.ID_ACCELERATION_X; id <= RemoteContext.ID_LIGHT; id++) {
+            if (mARContext.mRemoteComposeState.hasListener(id)) {
+                ids[count++] = id;
+            }
+        }
+        return count;
+    }
+
+    public void setExternalFloat(int id, float value) {
+        mARContext.loadFloat(id, value);
+    }
+
     public interface ClickCallbacks {
         void click(int id, String metadata);
     }
@@ -344,7 +359,9 @@
         mARContext.setAnimationEnabled(true);
         mARContext.currentTime = System.currentTimeMillis();
         mARContext.setDebug(mDebug);
+        float density = getContext().getResources().getDisplayMetrics().density;
         mARContext.useCanvas(canvas);
+        mARContext.setDensity(density);
         mARContext.mWidth = getWidth();
         mARContext.mHeight = getHeight();
         mDocument.paint(mARContext, mTheme);
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index a21bf9a..5c03c5c 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -161,6 +161,7 @@
                 "android_view_MotionPredictor.cpp",
                 "android_view_PointerIcon.cpp",
                 "android_view_SurfaceControl.cpp",
+                "android_view_SurfaceControlActivePictureListener.cpp",
                 "android_view_SurfaceControlHdrLayerInfoListener.cpp",
                 "android_view_WindowManagerGlobal.cpp",
                 "android_graphics_BLASTBufferQueue.cpp",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 821861e..00a6297 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -128,6 +128,7 @@
 extern int register_android_view_InputWindowHandle(JNIEnv* env);
 extern int register_android_view_Surface(JNIEnv* env);
 extern int register_android_view_SurfaceControl(JNIEnv* env);
+extern int register_android_view_SurfaceControlActivePictureListener(JNIEnv* env);
 extern int register_android_view_SurfaceControlHdrLayerInfoListener(JNIEnv* env);
 extern int register_android_view_SurfaceSession(JNIEnv* env);
 extern int register_android_view_CompositionSamplingListener(JNIEnv* env);
@@ -1563,6 +1564,7 @@
         REG_JNI(register_android_view_DisplayEventReceiver),
         REG_JNI(register_android_view_Surface),
         REG_JNI(register_android_view_SurfaceControl),
+        REG_JNI(register_android_view_SurfaceControlActivePictureListener),
         REG_JNI(register_android_view_SurfaceControlHdrLayerInfoListener),
         REG_JNI(register_android_view_SurfaceSession),
         REG_JNI(register_android_view_InputApplicationHandle),
diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp
index aebe7ea..0f78c9e 100644
--- a/core/jni/android_os_PerformanceHintManager.cpp
+++ b/core/jni/android_os_PerformanceHintManager.cpp
@@ -88,9 +88,10 @@
                         "Failed to find required symbol "
                         "APerformanceHint_getPreferredUpdateRateNanos!");
 
-    gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession");
+    gAPH_createSessionFn =
+            (APH_createSession)dlsym(handle_, "APerformanceHint_createSessionFromJava");
     LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr,
-                        "Failed to find required symbol APerformanceHint_createSession!");
+                        "Failed to find required symbol APerformanceHint_createSessionFromJava!");
 
     gAPH_updateTargetWorkDurationFn =
             (APH_updateTargetWorkDuration)dlsym(handle_,
@@ -106,9 +107,9 @@
                         "Failed to find required symbol "
                         "APerformanceHint_reportActualWorkDuration!");
 
-    gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession");
+    gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSessionFromJava");
     LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr,
-                        "Failed to find required symbol APerformanceHint_closeSession!");
+                        "Failed to find required symbol APerformanceHint_closeSessionFromJava!");
 
     gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint");
     LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr,
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 593b982..68e6420 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -55,6 +55,7 @@
 #include <ui/FrameStats.h>
 #include <ui/GraphicTypes.h>
 #include <ui/HdrCapabilities.h>
+#include <ui/PictureProfileHandle.h>
 #include <ui/Rect.h>
 #include <ui/Region.h>
 #include <ui/StaticDisplayInfo.h>
@@ -820,6 +821,21 @@
     transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys);
 }
 
+static void nativeSetPictureProfileId(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                      jlong surfaceControlObj, jlong pictureProfileId) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    SurfaceControl* const surfaceControl = reinterpret_cast<SurfaceControl*>(surfaceControlObj);
+    PictureProfileHandle handle(pictureProfileId);
+    transaction->setPictureProfileHandle(surfaceControl, handle);
+}
+
+static void nativeSetContentPriority(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                     jlong surfaceControlObj, jint priority) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    SurfaceControl* const surfaceControl = reinterpret_cast<SurfaceControl*>(surfaceControlObj);
+    transaction->setContentPriority(surfaceControl, priority);
+}
+
 static void nativeSetCachingHint(JNIEnv* env, jclass clazz, jlong transactionObj,
                                  jlong nativeObject, jint cachingHint) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -2351,6 +2367,20 @@
     return error == OK ? JNI_TRUE : JNI_FALSE;
 }
 
+static jint nativeGetMaxPictureProfiles(JNIEnv* env, jclass clazz) {
+    const auto displayIds = SurfaceComposerClient::SurfaceComposerClient::getPhysicalDisplayIds();
+    int largestMaxProfiles = 0;
+    for (auto displayId : displayIds) {
+        sp<IBinder> token = SurfaceComposerClient::getPhysicalDisplayToken(displayId);
+        int32_t maxProfiles = 0;
+        SurfaceComposerClient::getMaxLayerPictureProfiles(token, &maxProfiles);
+        if (maxProfiles > largestMaxProfiles) {
+            largestMaxProfiles = maxProfiles;
+        }
+    }
+    return largestMaxProfiles;
+}
+
 jlong nativeCreateTpc(JNIEnv* env, jclass clazz, jobject trustedPresentationCallback) {
     return reinterpret_cast<jlong>(
             new TrustedPresentationCallbackWrapper(env, trustedPresentationCallback));
@@ -2672,6 +2702,8 @@
                 (void*)nativeGetDefaultApplyToken },
     {"nativeBootFinished", "()Z",
             (void*)nativeBootFinished },
+    {"nativeGetMaxPictureProfiles", "()I",
+            (void*)nativeGetMaxPictureProfiles },
     {"nativeCreateTpc", "(Landroid/view/SurfaceControl$TrustedPresentationCallback;)J",
             (void*)nativeCreateTpc},
     {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer },
@@ -2683,6 +2715,8 @@
             (void*)nativeNotifyShutdown },
     {"nativeSetLuts", "(JJ[F[I[I[I[I)V", (void*)nativeSetLuts },
     {"nativeEnableDebugLogCallPoints", "(J)V", (void*)nativeEnableDebugLogCallPoints },
+    {"nativeSetPictureProfileId", "(JJJ)V", (void*)nativeSetPictureProfileId },
+    {"nativeSetContentPriority", "(JJI)V", (void*)nativeSetContentPriority },
         // clang-format on
 };
 
diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
new file mode 100644
index 0000000..91849c1
--- /dev/null
+++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "SurfaceControlActivePictureListener"
+
+#include <android/gui/BnActivePictureListener.h>
+#include <android_runtime/Log.h>
+#include <gui/ISurfaceComposer.h>
+#include <gui/SurfaceComposerClient.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/RefBase.h>
+
+#include "android_util_Binder.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+namespace {
+
+struct {
+    jclass clazz;
+    jmethodID onActivePicturesChanged;
+} gListenerClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID constructor;
+} gActivePictureClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID constructor;
+    jfieldID id;
+} gPictureProfileHandleClassInfo;
+
+struct SurfaceControlActivePictureListener : public gui::BnActivePictureListener {
+    SurfaceControlActivePictureListener(JNIEnv* env, jobject listener)
+          : mListener(env->NewGlobalRef(listener)) {
+        LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&mVm) != JNI_OK, "Failed to GetJavaVm");
+    }
+
+    binder::Status onActivePicturesChanged(
+            const std::vector<gui::ActivePicture>& activePictures) override {
+        JNIEnv* env = requireEnv();
+
+        ScopedLocalRef<jobjectArray> activePictureArrayObj(env);
+        activePictureArrayObj.reset(
+                env->NewObjectArray(activePictures.size(), gActivePictureClassInfo.clazz, NULL));
+        if (env->ExceptionCheck() || !activePictureArrayObj.get()) {
+            LOGE_EX(env);
+            LOG_ALWAYS_FATAL("Failed to create an active picture array.");
+        }
+
+        {
+            std::vector<ScopedLocalRef<jobject>> pictureProfileHandleObjs;
+            std::vector<ScopedLocalRef<jobject>> activePictureObjs;
+
+            for (size_t i = 0; i < activePictures.size(); ++i) {
+                pictureProfileHandleObjs.push_back(ScopedLocalRef<jobject>(env));
+                pictureProfileHandleObjs[i].reset(
+                        env->NewObject(gPictureProfileHandleClassInfo.clazz,
+                                       gPictureProfileHandleClassInfo.constructor,
+                                       activePictures[i].pictureProfileId));
+                if (env->ExceptionCheck() || !pictureProfileHandleObjs[i].get()) {
+                    LOGE_EX(env);
+                    LOG_ALWAYS_FATAL("Failed to create a picture profile handle.");
+                }
+                activePictureObjs.push_back(ScopedLocalRef<jobject>(env));
+                activePictureObjs[i].reset(env->NewObject(gActivePictureClassInfo.clazz,
+                                                          gActivePictureClassInfo.constructor,
+                                                          activePictures[i].layerId,
+                                                          activePictures[i].ownerUid,
+                                                          pictureProfileHandleObjs[i].get()));
+                if (env->ExceptionCheck() || !activePictureObjs[i].get()) {
+                    LOGE_EX(env);
+                    LOG_ALWAYS_FATAL("Failed to create an active picture.");
+                }
+                env->SetObjectArrayElement(activePictureArrayObj.get(), i,
+                                           activePictureObjs[i].get());
+            }
+
+            env->CallVoidMethod(mListener, gListenerClassInfo.onActivePicturesChanged,
+                                activePictureArrayObj.get());
+        }
+
+        if (env->ExceptionCheck()) {
+            ALOGE("SurfaceControlActivePictureListener.onActivePicturesChanged failed");
+            LOGE_EX(env);
+            env->ExceptionClear();
+        }
+        return binder::Status::ok();
+    }
+
+    status_t startListening() {
+        // TODO(b/337330263): Make SF multiple-listener capable
+        return SurfaceComposerClient::setActivePictureListener(this);
+    }
+
+    status_t stopListening() {
+        return SurfaceComposerClient::setActivePictureListener(nullptr);
+    }
+
+protected:
+    virtual ~SurfaceControlActivePictureListener() {
+        JNIEnv* env = requireEnv();
+        env->DeleteGlobalRef(mListener);
+    }
+
+    JNIEnv* requireEnv() {
+        JNIEnv* env = nullptr;
+        if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+            if (mVm->AttachCurrentThreadAsDaemon(&env, nullptr) != JNI_OK) {
+                LOG_ALWAYS_FATAL("Failed to AttachCurrentThread!");
+            }
+        }
+        return env;
+    }
+
+private:
+    jobject mListener;
+    JavaVM* mVm;
+};
+
+jlong nativeMakeAndStartListening(JNIEnv* env, jobject jthis) {
+    auto listener = sp<SurfaceControlActivePictureListener>::make(env, jthis);
+    status_t err = listener->startListening();
+    if (err != OK) {
+        auto errStr = statusToString(err);
+        jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
+                             "Failed to start listening, err = %d (%s)", err, errStr.c_str());
+        return 0;
+    }
+    SurfaceControlActivePictureListener* listenerRawPtr = listener.get();
+    listenerRawPtr->incStrong(0);
+    return static_cast<jlong>(reinterpret_cast<intptr_t>(listenerRawPtr));
+}
+
+static void destroy(SurfaceControlActivePictureListener* listener) {
+    listener->stopListening();
+    listener->decStrong(0);
+}
+
+static jlong nativeGetDestructor(JNIEnv* env, jobject clazz) {
+    return static_cast<jlong>(reinterpret_cast<intptr_t>(&destroy));
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeGetDestructor", "()J", (void*)nativeGetDestructor},
+        {"nativeMakeAndStartListening", "()J", (void*)nativeMakeAndStartListening}};
+} // namespace
+
+int register_android_view_SurfaceControlActivePictureListener(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/view/SurfaceControlActivePictureListener",
+                                       gMethods, NELEM(gMethods));
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    jclass listenerClazz = env->FindClass("android/view/SurfaceControlActivePictureListener");
+    gListenerClassInfo.clazz = MakeGlobalRefOrDie(env, listenerClazz);
+    gListenerClassInfo.onActivePicturesChanged =
+            env->GetMethodID(listenerClazz, "onActivePicturesChanged",
+                             "([Landroid/view/SurfaceControlActivePicture;)V");
+
+    gActivePictureClassInfo.clazz = static_cast<jclass>(
+            env->NewGlobalRef(env->FindClass("android/view/SurfaceControlActivePicture")));
+    gActivePictureClassInfo.constructor =
+            env->GetMethodID(gActivePictureClassInfo.clazz, "<init>",
+                             "(IILandroid/media/quality/PictureProfileHandle;)V");
+
+    gPictureProfileHandleClassInfo.clazz = static_cast<jclass>(
+            env->NewGlobalRef(env->FindClass("android/media/quality/PictureProfileHandle")));
+    gPictureProfileHandleClassInfo.constructor =
+            env->GetMethodID(gPictureProfileHandleClassInfo.clazz, "<init>", "(J)V");
+    return 0;
+}
+
+} // namespace android
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 0e4e22b..8042b30 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -161,6 +161,7 @@
         "android.app.flags-aconfig",
         "android.appwidget.flags-aconfig",
         "android.content.pm.flags-aconfig",
+        "android.media.audio-aconfig",
         "android.provider.flags-aconfig",
         "camera_platform_flags",
         "android.net.platform.flags-aconfig",
@@ -170,6 +171,7 @@
         "android.os.vibrator.flags-aconfig",
         "android.media.tv.flags-aconfig",
         "android.security.flags-aconfig",
+        "device_policy_aconfig_flags",
         "com.android.hardware.input.input-aconfig",
         "aconfig_trade_in_mode_flags",
         "art-aconfig-flags",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7fcbf19..dae182f 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2649,6 +2649,22 @@
         android:label="@string/permlab_getAccounts" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
 
+    <!-- @SystemApi Allows access to remove an account.
+         @FlaggedApi(android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED)
+         <p>Not for use by third-party applications.
+         @hide -->
+    <permission android:name="android.permission.REMOVE_ACCOUNTS"
+        android:protectionLevel="signature|role"
+        android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" />
+
+    <!-- @SystemApi Allows access to copy an account to another user.
+         @FlaggedApi(android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED)
+         <p>Not for use by third-party applications.
+         @hide -->
+    <permission android:name="android.permission.COPY_ACCOUNTS"
+        android:protectionLevel="signature|role"
+        android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" />
+
     <!-- Allows applications to call into AccountAuthenticators.
     <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.ACCOUNT_MANAGER"
@@ -4203,35 +4219,35 @@
     <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
         android:featureFlag="android.security.aapm_api"/>
 
-    <!-- Allows an application to read the state of the ForensicService
+    <!-- Allows an application to read the state of the IntrusionDetectionService
          @FlaggedApi(android.security.Flags.FLAG_AFL_API)
          @SystemApi
          @hide -->
-    <permission android:name="android.permission.READ_FORENSIC_STATE"
+    <permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"
         android:protectionLevel="signature|privileged" />
-    <uses-permission android:name="android.permission.READ_FORENSIC_STATE"
+    <uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"/>
 
-    <!-- Allows an application to change the state of the ForensicService
+    <!-- Allows an application to change the state of the IntrusionDetectionService
          @FlaggedApi(android.security.Flags.FLAG_AFL_API)
          @SystemApi
          @hide -->
-    <permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+    <permission android:name="android.permission.MANAGE_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"
         android:protectionLevel="signature|privileged" />
-    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+    <uses-permission android:name="android.permission.MANAGE_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"/>
 
-    <!-- Must be required by any ForensicEventTransportService to ensure that
+    <!-- Must be required by any IntrusionDetectionEventTransportService to ensure that
          only the system can bind to it.
          @FlaggedApi(android.security.Flags.FLAG_AFL_API)
          @SystemApi
          @hide -->
-    <permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"
+    <permission android:name="android.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE"
         android:featureFlag="android.security.afl_api"
         android:protectionLevel="signature" />
-    <uses-permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"
+    <uses-permission android:name="android.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE"
         android:featureFlag="android.security.afl_api"/>
 
     <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
@@ -6492,6 +6508,15 @@
     <permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT"
         android:protectionLevel="signature|privileged|role" />
 
+    <!-- @SystemApi Allows an application to bypass concurrency restrictions while
+        recording audio. For example, apps with this permission can continue to record
+        while a voice call is active.</p>
+     @FlaggedApi(android.media.audio.Flags.FLAG_CONCURRENT_AUDIO_RECORD_BYPASS_PERMISSION)
+     @hide -->
+    <permission android:name="android.permission.BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION"
+        android:featureFlag="android.media.audio.concurrent_audio_record_bypass_permission"
+        android:protectionLevel="signature|privileged|role" />
+
     <!-- @SystemApi Allows an application to capture audio for hotword detection.
          <p>Not for use by third-party applications.</p>
          @hide -->
@@ -8651,6 +8676,17 @@
     <permission android:name="android.permission.SETUP_FSVERITY"
                 android:protectionLevel="signature|privileged"/>
 
+    <!-- @SystemApi
+        @FlaggedApi(android.security.Flags.FLAG_SECURE_LOCKDOWN)
+        Allows an application to lock down the device into an enhanced security state.
+        <p>Not for use by third-party applications.
+        <p>Protection level: signature|privileged
+        @hide
+    -->
+    <permission android:name="android.permission.MANAGE_SECURE_LOCK_DEVICE"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.security.secure_lockdown" />
+
     <!-- Allows app to enter trade-in-mode.
         <p>Protection level: signature|privileged
         @hide
diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml
new file mode 100644
index 0000000..e4ff835
--- /dev/null
+++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2014 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
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/status_bar_latest_event_content"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:clipChildren="false"
+    android:tag="headsUp"
+    >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:clipChildren="false"
+        android:orientation="vertical"
+        >
+
+        <include
+            layout="@layout/notification_2025_template_collapsed_base"
+            android:id="@null"
+            />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="-20dp"
+            android:clipChildren="false"
+            android:orientation="vertical"
+            >
+
+            <ViewStub
+                android:layout="@layout/notification_material_reply_text"
+                android:id="@+id/notification_material_reply_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                />
+
+            <include
+                layout="@layout/notification_template_smart_reply_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/notification_2025_content_margin_start"
+                android:layout_marginEnd="@dimen/notification_content_margin_end"
+                android:layout_marginTop="@dimen/notification_content_margin"
+                />
+
+            <include layout="@layout/notification_material_action_list" />
+        </LinearLayout>
+    </LinearLayout>
+</FrameLayout>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3f4ea2d..48ce8af 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7184,6 +7184,10 @@
          screen. -->
     <bool name="config_dragToMaximizeInDesktopMode">false</bool>
 
+    <!-- Whether showing the app handle is supported on this device.
+         If config_isDesktopModeSupported, then this has no effect -->
+    <bool name="config_enableAppHandle">false</bool>
+
     <!-- Frame rate compatibility value for Wallpaper
          FRAME_RATE_COMPATIBILITY_MIN (102) is used by default for lower power consumption -->
     <integer name="config_wallpaperFrameRateCompatibility">102</integer>
@@ -7225,8 +7229,8 @@
     <!-- Package for opening identity check settings page [CHAR LIMIT=NONE] [DO NOT TRANSLATE] -->
     <string name="identity_check_settings_package_name">com\u002eandroid\u002esettings</string>
 
-    <!-- The name of the service for forensic event transport. -->
-    <string name="config_forensicEventTransport" translatable="false"></string>
+    <!-- The name of the service for intrusion detection event transport. -->
+    <string name="config_intrusionDetectionEventTransport" translatable="false"></string>
 
     <!-- Whether to enable fp unlock when screen turns off on udfps devices -->
     <bool name="config_screen_off_udfps_enabled">false</bool>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 4ec27a3..6378d5a 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -484,7 +484,7 @@
 
     <!-- Whether to show the system notification to users whenever there is a change
      in the satellite availability state at the current location. -->
-    <bool name="config_satellite_should_notify_availability">false</bool>
+    <bool name="config_satellite_should_notify_availability">true</bool>
     <java-symbol type="bool" name="config_satellite_should_notify_availability" />
 
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a2a19a2..3ecbf49 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2390,6 +2390,7 @@
   <java-symbol type="layout" name="notification_material_action_list" />
   <java-symbol type="layout" name="notification_material_action_tombstone" />
   <java-symbol type="layout" name="notification_2025_template_collapsed_base" />
+  <java-symbol type="layout" name="notification_2025_template_heads_up_base" />
   <java-symbol type="layout" name="notification_2025_template_header" />
   <java-symbol type="layout" name="notification_template_material_base" />
   <java-symbol type="layout" name="notification_template_material_heads_up_base" />
@@ -5317,6 +5318,91 @@
   <java-symbol type="integer" name="config_accumulatedBatteryUsageStatsSpanSize" />
 
   <!--Dynamic Tokens-->
+  <java-symbol name="materialColorBackground" type="color"/>
+  <java-symbol name="materialColorControlActivated" type="color"/>
+  <java-symbol name="materialColorControlHighlight" type="color"/>
+  <java-symbol name="materialColorControlNormal" type="color"/>
+  <java-symbol name="materialColorError" type="color"/>
+  <java-symbol name="materialColorErrorContainer" type="color"/>
+  <java-symbol name="materialColorInverseOnSurface" type="color"/>
+  <java-symbol name="materialColorInversePrimary" type="color"/>
+  <java-symbol name="materialColorInverseSurface" type="color"/>
+  <java-symbol name="materialColorOnBackground" type="color"/>
+  <java-symbol name="materialColorOnError" type="color"/>
+  <java-symbol name="materialColorOnErrorContainer" type="color"/>
+  <java-symbol name="materialColorOnPrimary" type="color"/>
+  <java-symbol name="materialColorOnPrimaryContainer" type="color"/>
+  <java-symbol name="materialColorOnSecondary" type="color"/>
+  <java-symbol name="materialColorOnSecondaryContainer" type="color"/>
+  <java-symbol name="materialColorOnSurface" type="color"/>
+  <java-symbol name="materialColorOnSurfaceVariant" type="color"/>
+  <java-symbol name="materialColorOnTertiary" type="color"/>
+  <java-symbol name="materialColorOnTertiaryContainer" type="color"/>
+  <java-symbol name="materialColorOutline" type="color"/>
+  <java-symbol name="materialColorOutlineVariant" type="color"/>
+  <java-symbol name="materialColorPaletteKeyColorNeutral" type="color"/>
+  <java-symbol name="materialColorPaletteKeyColorNeutralVariant" type="color"/>
+  <java-symbol name="materialColorPaletteKeyColorPrimary" type="color"/>
+  <java-symbol name="materialColorPaletteKeyColorSecondary" type="color"/>
+  <java-symbol name="materialColorPaletteKeyColorTertiary" type="color"/>
+  <java-symbol name="materialColorPrimary" type="color"/>
+  <java-symbol name="materialColorPrimaryContainer" type="color"/>
+  <java-symbol name="materialColorScrim" type="color"/>
+  <java-symbol name="materialColorSecondary" type="color"/>
+  <java-symbol name="materialColorSecondaryContainer" type="color"/>
+  <java-symbol name="materialColorShadow" type="color"/>
+  <java-symbol name="materialColorSurface" type="color"/>
+  <java-symbol name="materialColorSurfaceBright" type="color"/>
+  <java-symbol name="materialColorSurfaceContainer" type="color"/>
+  <java-symbol name="materialColorSurfaceContainerHigh" type="color"/>
+  <java-symbol name="materialColorSurfaceContainerHighest" type="color"/>
+  <java-symbol name="materialColorSurfaceContainerLow" type="color"/>
+  <java-symbol name="materialColorSurfaceContainerLowest" type="color"/>
+  <java-symbol name="materialColorSurfaceDim" type="color"/>
+  <java-symbol name="materialColorSurfaceTint" type="color"/>
+  <java-symbol name="materialColorSurfaceVariant" type="color"/>
+  <java-symbol name="materialColorTertiary" type="color"/>
+  <java-symbol name="materialColorTertiaryContainer" type="color"/>
+  <java-symbol name="materialColorTextHintInverse" type="color"/>
+  <java-symbol name="materialColorTextPrimaryInverse" type="color"/>
+  <java-symbol name="materialColorTextPrimaryInverseDisableOnly" type="color"/>
+  <java-symbol name="materialColorTextSecondaryAndTertiaryInverse" type="color"/>
+  <java-symbol name="materialColorTextSecondaryAndTertiaryInverseDisabled" type="color"/>
+  <java-symbol name="materialColorOnPrimaryFixed" type="color"/>
+  <java-symbol name="materialColorOnPrimaryFixedVariant" type="color"/>
+  <java-symbol name="materialColorOnSecondaryFixed" type="color"/>
+  <java-symbol name="materialColorOnSecondaryFixedVariant" type="color"/>
+  <java-symbol name="materialColorOnTertiaryFixed" type="color"/>
+  <java-symbol name="materialColorOnTertiaryFixedVariant" type="color"/>
+  <java-symbol name="materialColorPrimaryFixed" type="color"/>
+  <java-symbol name="materialColorPrimaryFixedDim" type="color"/>
+  <java-symbol name="materialColorSecondaryFixed" type="color"/>
+  <java-symbol name="materialColorSecondaryFixedDim" type="color"/>
+  <java-symbol name="materialColorTertiaryFixed" type="color"/>
+  <java-symbol name="materialColorTertiaryFixedDim" type="color"/>
+  <java-symbol name="customColorBrandA" type="color"/>
+  <java-symbol name="customColorBrandB" type="color"/>
+  <java-symbol name="customColorBrandC" type="color"/>
+  <java-symbol name="customColorBrandD" type="color"/>
+  <java-symbol name="customColorClockHour" type="color"/>
+  <java-symbol name="customColorClockMinute" type="color"/>
+  <java-symbol name="customColorClockSecond" type="color"/>
+  <java-symbol name="customColorOnShadeActive" type="color"/>
+  <java-symbol name="customColorOnShadeActiveVariant" type="color"/>
+  <java-symbol name="customColorOnShadeInactive" type="color"/>
+  <java-symbol name="customColorOnShadeInactiveVariant" type="color"/>
+  <java-symbol name="customColorOnThemeApp" type="color"/>
+  <java-symbol name="customColorOverviewBackground" type="color"/>
+  <java-symbol name="customColorShadeActive" type="color"/>
+  <java-symbol name="customColorShadeDisabled" type="color"/>
+  <java-symbol name="customColorShadeInactive" type="color"/>
+  <java-symbol name="customColorThemeApp" type="color"/>
+  <java-symbol name="customColorThemeAppRing" type="color"/>
+  <java-symbol name="customColorThemeNotif" type="color"/>
+  <java-symbol name="customColorUnderSurface" type="color"/>
+  <java-symbol name="customColorWeatherTemp" type="color"/>
+  <java-symbol name="customColorWidgetBackground" type="color"/>
+
   <java-symbol type="attr" name="materialColorBackground"/>
   <java-symbol type="attr" name="materialColorControlActivated"/>
   <java-symbol type="attr" name="materialColorControlHighlight"/>
@@ -5634,6 +5720,9 @@
        screen. -->
   <java-symbol type="bool" name="config_dragToMaximizeInDesktopMode" />
 
+  <!-- Whether showing the app handle is supported on this device -->
+  <java-symbol type="bool" name="config_enableAppHandle" />
+
   <!-- Frame rate compatibility value for Wallpaper -->
   <java-symbol type="integer" name="config_wallpaperFrameRateCompatibility" />
 
@@ -5702,8 +5791,8 @@
   <java-symbol type="string" name="identity_check_settings_action" />
   <java-symbol type="string" name="identity_check_settings_package_name" />
 
-  <!-- Forensic event transport -->
-  <java-symbol type="string" name="config_forensicEventTransport" />
+  <!-- Intrusion detection event transport -->
+  <java-symbol type="string" name="config_intrusionDetectionEventTransport" />
 
   <!-- Fingerprint screen off unlock config -->
   <java-symbol type="bool" name="config_screen_off_udfps_enabled" />
diff --git a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
index 75aca1b..7ce2ed8 100644
--- a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
+++ b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
@@ -23,10 +23,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Objects;
@@ -39,8 +40,8 @@
     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()
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
             .setSystemPropertyMutable(KEY, null)
             .setSystemPropertyMutable(UNSET_KEY, null)
             .setSystemPropertyMutable(PERSIST_KEY, null)
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index ccc5108..5f3754b 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -418,20 +418,45 @@
         // Effects created via waveformEnvelopeBuilder are not expected to be converted to long[]
         // patterns, as they are not configured to always play with the default amplitude.
         VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 40)
                 .build();
 
         assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
 
         effect = new VibrationEffect.WaveformEnvelopeBuilder()
                 .setInitialFrequencyHz(/*initialFrequencyHz=*/ 60)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 40)
+                .build();
+
+        assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void computeLegacyPattern_effectsViaBasicEnvelopeBuilder() {
+        // Effects created via BasicEnvelopeBuilder are not expected to be converted to long[]
+        // patterns, as they are not configured to always play with the default amplitude.
+        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(/*intensity=*/ 0.1f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 20)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 50)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 80)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 40)
+                .build();
+
+        assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
+
+        effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.1f)
+                .addControlPoint(/*intensity=*/ 0.2f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 20)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 50)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 80)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 40)
                 .build();
 
         assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
@@ -646,43 +671,43 @@
     @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testValidateWaveformEnvelopeBuilder() {
         new VibrationEffect.WaveformEnvelopeBuilder()
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 40)
                 .build()
                 .validate();
 
         new VibrationEffect.WaveformEnvelopeBuilder()
                 .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 40)
                 .build()
                 .validate();
 
         VibrationEffect.createRepeatingEffect(
                 /*preamble=*/ new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f,
-                                /*timeMillis=*/ 50)
+                                /*durationMillis=*/ 50)
                         .build(),
                 /*repeatingEffect=*/ new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .addControlPoint(/*amplitude=*/ 0.5f, /*frequencyHz=*/ 150f,
-                                /*timeMillis=*/ 100)
+                                /*durationMillis=*/ 100)
                         .build()
         ).validate();
 
         VibrationEffect.createRepeatingEffect(
                 /*effect=*/ new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .addControlPoint(/*amplitude=*/ 0.5f, /*frequencyHz=*/ 150f,
-                                /*timeMillis=*/ 100)
+                                /*durationMillis=*/ 100)
                         .build()
         ).validate();
 
@@ -691,25 +716,25 @@
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
-                                /*timeMillis=*/ 0)
+                                /*durationMillis=*/ 0)
                         .build()
                         .validate());
 
@@ -721,39 +746,156 @@
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
                         .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
                         .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
                         .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
                         .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
-                                /*timeMillis=*/ 0)
+                                /*durationMillis=*/ 0)
                         .build()
                         .validate());
         assertThrows(IllegalArgumentException.class,
                 () -> new VibrationEffect.WaveformEnvelopeBuilder()
                         .setInitialFrequencyHz(/*initialFrequencyHz=*/ 0)
                         .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
-                                /*timeMillis=*/ 20)
+                                /*durationMillis=*/ 20)
                         .build().validate());
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testValidateBasicEnvelopeBuilder() {
+        new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(/*intensity=*/ 0.1f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 20)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 50)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 80)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 40)
+                .build()
+                .validate();
+
+        new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.1f)
+                .addControlPoint(/*intensity=*/ 0.2f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 20)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 50)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 80)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 40)
+                .build()
+                .validate();
+
+        VibrationEffect.createRepeatingEffect(
+                /*preamble=*/ new VibrationEffect.BasicEnvelopeBuilder()
+                        // intensity, sharpness, durationMillis
+                        .addControlPoint(0.3f, 0.5f, 60)
+                        .addControlPoint(0.0f, 0.5f, 100)
+                        .build(),
+                /*repeatingEffect=*/ new VibrationEffect.BasicEnvelopeBuilder()
+                        // intensity, sharpness, durationMillis
+                        .addControlPoint(0.5f, 0.8f, 60)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+        ).validate();
+
+        VibrationEffect.createRepeatingEffect(
+                new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        // intensity, sharpness, durationMillis
+                        .addControlPoint(0.5f, 0.8f, 60)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+        ).validate();
+
+        assertThrows(IllegalStateException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder().build().validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                        .addControlPoint(-1.0f, 0.5f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                        .addControlPoint(1.1f, 0.5f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                        .addControlPoint(0.8f, -0.1f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                        .addControlPoint(0.8f, 0.5f, 0)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        // Waveform effects created using the basic builder must end with a zero intensity
+        // control point.
+        assertThrows(IllegalStateException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                        .addControlPoint(0.8f, 0.5f, 100)
+                        .build()
+                        .validate());
+
+        assertThrows(IllegalStateException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/0.2f).build().validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        .addControlPoint(-1.0f, 0.5f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        .addControlPoint(1.1f, 0.5f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        .addControlPoint(0.8f, -0.1f, 20)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        .addControlPoint(0.8f, 0.5f, 0)
+                        .addControlPoint(0.0f, 0.3f, 100)
+                        .build()
+                        .validate());
+        // Waveform effects created using the basic builder must end with a zero intensity
+        // control point.
+        assertThrows(IllegalStateException.class,
+                () -> new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                        .addControlPoint(0.8f, 0.5f, 100)
+                        .build()
+                        .validate());
+    }
+
+    @Test
     public void testValidateWaveformBuilder() {
         // Cover builder methods
         VibrationEffect.startWaveform(targetAmplitude(1))
@@ -1395,18 +1537,36 @@
     @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testIsHapticFeedbackCandidate_longEnvelopeEffects_notCandidates() {
         assertFalse(new VibrationEffect.WaveformEnvelopeBuilder()
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+                //amplitude, frequencyHz, durationMillis
+                .addControlPoint(0.0f, 60f, 200)
+                .addControlPoint(0.3f, 100f, 500)
+                .addControlPoint(0.4f, 120f, 800)
+                .addControlPoint(0.0f, 120f, 400)
                 .build()
                 .isHapticFeedbackCandidate());
         assertFalse(new VibrationEffect.WaveformEnvelopeBuilder()
                 .setInitialFrequencyHz(/*initialFrequencyHz=*/ 40)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+                //amplitude, frequencyHz, durationMillis
+                .addControlPoint(0.0f, 60f, 200)
+                .addControlPoint(0.3f, 100f, 500)
+                .addControlPoint(0.4f, 120f, 800)
+                .addControlPoint(0.0f, 120f, 400)
+                .build()
+                .isHapticFeedbackCandidate());
+
+        assertFalse(new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(/*intensity=*/ 0.1f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 200)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 500)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 800)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 400)
+                .build()
+                .isHapticFeedbackCandidate());
+        assertFalse(new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                .addControlPoint(/*intensity=*/ 0.1f, /*sharpness=*/ 0.2f, /*durationMillis=*/ 200)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 500)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 800)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 400)
                 .build()
                 .isHapticFeedbackCandidate());
     }
@@ -1428,16 +1588,31 @@
     @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testIsHapticFeedbackCandidate_shortEnvelopeEffects_areCandidates() {
         assertTrue(new VibrationEffect.WaveformEnvelopeBuilder()
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
+                //amplitude, frequencyHz, durationMillis
+                .addControlPoint(0.3f, 100f, 500)
+                .addControlPoint(0.4f, 120f, 400)
+                .addControlPoint(0.0f, 120f, 100)
                 .build()
                 .isHapticFeedbackCandidate());
         assertTrue(new VibrationEffect.WaveformEnvelopeBuilder()
                 .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
+                .addControlPoint(0.3f, 100f, 500)
+                .addControlPoint(0.4f, 120f, 400)
+                .addControlPoint(0.0f, 120f, 100)
+                .build()
+                .isHapticFeedbackCandidate());
+
+        assertTrue(new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 500)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 400)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 100)
+                .build()
+                .isHapticFeedbackCandidate());
+        assertTrue(new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 0.2f)
+                .addControlPoint(/*intensity=*/ 0.3f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 500)
+                .addControlPoint(/*intensity=*/ 0.4f, /*sharpness=*/ 0.5f, /*durationMillis=*/ 400)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 0.3f, /*durationMillis=*/ 100)
                 .build()
                 .isHapticFeedbackCandidate());
     }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index fea7cb4..836870e 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -533,6 +533,8 @@
         <permission name="com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY" />
         <!-- Permission required for ATS test - CarDevicePolicyManagerTest -->
         <permission name="android.permission.LOCK_DEVICE" />
+        <!-- Permission required for AuthenticationPolicyManagerTest -->
+        <permission name="android.permission.MANAGE_SECURE_LOCK_DEVICE" />
         <!-- Permissions required for CTS test - CtsSafetyCenterTestCases -->
         <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
         <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
@@ -603,9 +605,9 @@
         <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
         <permission name="android.permission.READ_SYSTEM_PREFERENCES" />
         <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" />
-        <!-- Permission required for CTS test - ForensicManagerTest -->
-        <permission name="android.permission.READ_FORENSIC_STATE" />
-        <permission name="android.permission.MANAGE_FORENSIC_STATE" />
+        <!-- Permission required for CTS test - IntrusionDetectionManagerTest -->
+        <permission name="android.permission.READ_INTRUSION_DETECTION_STATE" />
+        <permission name="android.permission.MANAGE_INTRUSION_DETECTION_STATE" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 5e41865..375968a 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -113,7 +113,7 @@
                     style="?android:attr/buttonBarButtonStyle"
                     android:layout_width="41dp"
                     android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
-                    android:layout_marginRight="4dp"
+                    android:layout_marginEnd="4dp"
                     android:background="@drawable/desktop_mode_maximize_menu_button_background"
                     android:importantForAccessibility="yes"
                     android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 7078d66..21ec84d 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -504,17 +504,15 @@
     <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen>
     <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. -->
     <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen>
-    <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. -->
-    <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom">8dp</dimen>
     <!-- The vertical padding between the outline and fill of the maximize menu restore button. -->
     <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen>
     <!-- The horizontal padding between the outline and fill of the maximize menu restore button. -->
-    <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen>
+    <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">15dp</dimen>
     <!-- The padding between the outline and fill of the maximize menu immersive button. -->
-    <dimen name="desktop_mode_maximize_menu_immersive_button_fill_padding">4dp</dimen>
+    <dimen name="desktop_mode_maximize_menu_immersive_button_fill_padding">0dp</dimen>
 
     <!-- The corner radius of the maximize menu. -->
-    <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen>
+    <dimen name="desktop_mode_maximize_menu_corner_radius">16dp</dimen>
 
     <!-- The radius of the Maximize menu shadow. -->
     <dimen name="desktop_mode_maximize_menu_shadow_radius">8dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 012579a..468c345 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -318,7 +318,7 @@
     <!-- Maximize menu maximize button string. -->
     <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
     <!-- Maximize menu snap buttons string. -->
-    <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
+    <string name="desktop_mode_maximize_menu_snap_text">Resize</string>
     <!-- Snap resizing non-resizable string. -->
     <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string>
     <!-- Accessibility text for the Maximize Menu's immersive button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 55cda78..597a921 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -29,6 +29,7 @@
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowDrawsSystemBarBackgrounds">true</item>
         <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
     </style>
 
     <style name="Animation.ForcedResizable" parent="@android:style/Animation">
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 6bc995f1..04c17e5 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -191,6 +191,23 @@
     }
 
     /**
+     * @return {@code true} if this device is requesting to show the app handle despite non
+     * necessarily enabling desktop mode
+     */
+    public static boolean overridesShowAppHandle(@NonNull Context context) {
+        return Flags.showAppHandleLargeScreens()
+                && context.getResources().getBoolean(R.bool.config_enableAppHandle);
+    }
+
+    /**
+     * @return {@code true} if the app handle should be shown because desktop mode is enabled or
+     * the device is overriding {@code R.bool.config_enableAppHandle}
+     */
+    public static boolean canEnterDesktopModeOrShowAppHandle(@NonNull Context context) {
+        return canEnterDesktopMode(context) || overridesShowAppHandle(context);
+    }
+
+    /**
      * Return {@code true} if the override desktop density is enabled and valid.
      */
     public static boolean useDesktopOverrideDensity() {
@@ -264,5 +281,8 @@
         SystemProperties.Handle maxTaskLimitHandle = SystemProperties.find(MAX_TASK_LIMIT_SYS_PROP);
         pw.print(innerPrefix); pw.print("maxTaskLimit sysprop=");
         pw.println(maxTaskLimitHandle == null ? "null" : maxTaskLimitHandle.getInt(/* def= */ -1));
+
+        pw.print(innerPrefix); pw.print("showAppHandle config override=");
+        pw.print(context.getResources().getBoolean(R.bool.config_enableAppHandle));
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 603a9ec..b82496e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1327,7 +1327,11 @@
 
     /** Promote the provided bubble from the overflow view. */
     public void promoteBubbleFromOverflow(Bubble bubble) {
-        mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
+        if (isShowingAsBubbleBar()) {
+            mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR);
+        } else {
+            mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
+        }
         ProtoLog.d(WM_SHELL_BUBBLES, "promoteBubbleFromOverflow=%s", bubble.getKey());
         bubble.setInflateSynchronously(mInflateSynchronously);
         bubble.setShouldAutoExpand(true);
@@ -1350,11 +1354,7 @@
         if (BubbleOverflow.KEY.equals(key)) {
             mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
             mLayerView.showExpandedView(mBubbleData.getOverflow());
-            if (wasExpanded) {
-                mLogger.log(BubbleLogger.Event.BUBBLE_BAR_BUBBLE_SWITCHED);
-            } else {
-                mLogger.log(BubbleLogger.Event.BUBBLE_BAR_EXPANDED);
-            }
+            mLogger.log(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_SELECTED);
             return;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 4de9dfa..2945691 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -759,7 +759,9 @@
                 if (b != null) {
                     b.stopInflation();
                 }
-                mLogger.logOverflowRemove(b, reason);
+                if (!mPositioner.isShowingInBubbleBar()) {
+                    mLogger.logStackOverflowRemove(b, reason);
+                }
                 mOverflowBubbles.remove(b);
                 mStateChange.bubbleRemoved(b, reason);
                 mStateChange.removedOverflowBubble = b;
@@ -802,6 +804,27 @@
             setNewSelectedIndex(indexToRemove);
         }
         maybeSendDeleteIntent(reason, bubbleToRemove);
+
+        if (mPositioner.isShowingInBubbleBar()) {
+            logBubbleBarBubbleRemoved(bubbleToRemove, reason);
+        }
+    }
+
+    private void logBubbleBarBubbleRemoved(Bubble bubble, @DismissReason int reason) {
+        switch (reason) {
+            case Bubbles.DISMISS_NOTIF_CANCEL:
+                mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_CANCELED);
+                break;
+            case Bubbles.DISMISS_TASK_FINISHED:
+                mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_ACTIVITY_FINISH);
+                break;
+            case Bubbles.DISMISS_BLOCKED:
+            case Bubbles.DISMISS_NO_LONGER_BUBBLE:
+                mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED);
+                break;
+            default:
+                // skip logging other events
+        }
     }
 
     private void setNewSelectedIndex(int indexOfSelected) {
@@ -862,7 +885,7 @@
             return;
         }
         ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey());
-        mLogger.logOverflowAdd(bubble, reason);
+        mLogger.logOverflowAdd(bubble, mPositioner.isShowingInBubbleBar(), reason);
         if (mOverflowBubbles.isEmpty()) {
             mStateChange.showOverflowChanged = true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
index 3663073..347df33 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
@@ -182,10 +182,12 @@
     }
 
     /**
+     * Log when a bubble is removed from overflow in stack view
+     *
      * @param b Bubble removed from overflow
      * @param r Reason that bubble was removed
      */
-    public void logOverflowRemove(Bubble b, @Bubbles.DismissReason int r) {
+    public void logStackOverflowRemove(Bubble b, @Bubbles.DismissReason int r) {
         if (r == Bubbles.DISMISS_NOTIF_CANCEL) {
             log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL);
         } else if (r == Bubbles.DISMISS_GROUP_CANCELLED) {
@@ -201,13 +203,19 @@
      * @param b Bubble added to overflow
      * @param r Reason that bubble was added to overflow
      */
-    public void logOverflowAdd(Bubble b, @Bubbles.DismissReason int r) {
-        if (r == Bubbles.DISMISS_AGED) {
-            log(b, Event.BUBBLE_OVERFLOW_ADD_AGED);
-        } else if (r == Bubbles.DISMISS_USER_GESTURE) {
-            log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE);
-        } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) {
-            log(b, Event.BUBBLE_OVERFLOW_RECOVER);
+    public void logOverflowAdd(Bubble b, boolean bubbleBar, @Bubbles.DismissReason int r) {
+        if (bubbleBar) {
+            if (r == Bubbles.DISMISS_AGED) {
+                log(b, Event.BUBBLE_BAR_OVERFLOW_ADD_AGED);
+            }
+        } else {
+            if (r == Bubbles.DISMISS_AGED) {
+                log(b, Event.BUBBLE_OVERFLOW_ADD_AGED);
+            } else if (r == Bubbles.DISMISS_USER_GESTURE) {
+                log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE);
+            } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) {
+                log(b, Event.BUBBLE_OVERFLOW_RECOVER);
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index c386c93..068b2d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -830,6 +830,13 @@
         mShowingInBubbleBar = showingInBubbleBar;
     }
 
+    /**
+     * Whether bubbles ar showing in the bubble bar from launcher.
+     */
+    boolean isShowingInBubbleBar() {
+        return mShowingInBubbleBar;
+    }
+
     public void setBubbleBarLocation(BubbleBarLocation location) {
         mBubbleBarLocation = location;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index dab30b0..0f21756 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -88,6 +88,7 @@
 import com.android.wm.shell.splitscreen.StageTaskListener;
 
 import java.io.PrintWriter;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
@@ -139,15 +140,21 @@
     private final Rect mTempRect = new Rect();
     private final Rect mRootBounds = new Rect();
     private final Rect mDividerBounds = new Rect();
-    // Bounds1 final position should be always at top or left
-    private final Rect mBounds1 = new Rect();
-    // Bounds2 final position should be always at bottom or right
-    private final Rect mBounds2 = new Rect();
+    /**
+     * A list of stage bounds, kept in order from top/left to bottom/right. These are the sizes of
+     * the app surfaces, not necessarily the same as the size of the rendered content.
+     * See {@link #mContentBounds}.
+     */
+    private final List<Rect> mStageBounds = List.of(new Rect(), new Rect());
+    /**
+     * A list of app content bounds, kept in order from top/left to bottom/right. These are the
+     * sizes of the rendered app contents, not necessarily the same as the size of the drawn app
+     * surfaces. See {@link #mStageBounds}.
+     */
+    private final List<Rect> mContentBounds = List.of(new Rect(), new Rect());
     // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
     // flicker next time active split screen.
     private final Rect mInvisibleBounds = new Rect();
-    private final Rect mWinBounds1 = new Rect();
-    private final Rect mWinBounds2 = new Rect();
     private final SplitLayoutHandler mSplitLayoutHandler;
     private final SplitWindowManager mSplitWindowManager;
     private final DisplayController mDisplayController;
@@ -233,26 +240,26 @@
         mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
     }
 
-    /** Gets bounds of the primary split with screen based coordinate. */
-    public Rect getBounds1() {
-        return new Rect(mBounds1);
+    /** Gets the bounds of the top/left app in screen-based coordinates. */
+    public Rect getTopLeftBounds() {
+        return mStageBounds.getFirst();
     }
 
-    /** Gets bounds of the primary split with parent based coordinate. */
-    public Rect getRefBounds1() {
-        Rect outBounds = getBounds1();
+    /** Gets the bounds of the bottom/right app in screen-based coordinates. */
+    public Rect getBottomRightBounds() {
+        return mStageBounds.getLast();
+    }
+
+    /** Gets the bounds of the top/left app in parent-based coordinates. */
+    public Rect getTopLeftRefBounds() {
+        Rect outBounds = getTopLeftBounds();
         outBounds.offset(-mRootBounds.left, -mRootBounds.top);
         return outBounds;
     }
 
-    /** Gets bounds of the secondary split with screen based coordinate. */
-    public Rect getBounds2() {
-        return new Rect(mBounds2);
-    }
-
-    /** Gets bounds of the secondary split with parent based coordinate. */
-    public Rect getRefBounds2() {
-        final Rect outBounds = getBounds2();
+    /** Gets the bounds of the bottom/right app in parent-based coordinates. */
+    public Rect getBottomRightRefBounds() {
+        Rect outBounds = getBottomRightBounds();
         outBounds.offset(-mRootBounds.left, -mRootBounds.top);
         return outBounds;
     }
@@ -274,31 +281,42 @@
         return outBounds;
     }
 
-    /** Gets bounds of the primary split with screen based coordinate on the param Rect. */
-    public void getBounds1(Rect rect) {
-        rect.set(mBounds1);
+    /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */
+    public void copyTopLeftBounds(Rect rect) {
+        rect.set(getTopLeftBounds());
     }
 
-    /** Gets bounds of the primary split with parent based coordinate on the param Rect. */
-    public void getRefBounds1(Rect rect) {
-        getBounds1(rect);
+    /** Copies the top/left bounds to the provided Rect (parent-based coordinates). */
+    public void copyTopLeftRefBounds(Rect rect) {
+        copyTopLeftBounds(rect);
         rect.offset(-mRootBounds.left, -mRootBounds.top);
     }
 
-    /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */
-    public void getBounds2(Rect rect) {
-        rect.set(mBounds2);
+    /** Copies the bottom/right bounds to the provided Rect (screen-based coordinates). */
+    public void copyBottomRightBounds(Rect rect) {
+        rect.set(getBottomRightBounds());
     }
 
-    /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */
-    public void getRefBounds2(Rect rect) {
-        getBounds2(rect);
+    /** Copies the bottom/right bounds to the provided Rect (parent-based coordinates). */
+    public void copyBottomRightRefBounds(Rect rect) {
+        copyBottomRightBounds(rect);
         rect.offset(-mRootBounds.left, -mRootBounds.top);
     }
 
-    /** Gets root bounds of the whole split layout on the param Rect. */
-    public void getRootBounds(Rect rect) {
-        rect.set(mRootBounds);
+    /**
+     * Gets the content bounds of the top/left app (the bounds of where the app contents would be
+     * drawn). Might be larger than the available surface space.
+     */
+    public Rect getTopLeftContentBounds() {
+        return mContentBounds.getFirst();
+    }
+
+    /**
+     * Gets the content bounds of the bottom/right app (the bounds of where the app contents would
+     * be drawn). Might be larger than the available surface space.
+     */
+    public Rect getBottomRightContentBounds() {
+        return mContentBounds.getLast();
     }
 
     /** Gets bounds of divider window with screen based coordinate on the param Rect. */
@@ -340,8 +358,10 @@
      */
     public float getDividerPositionAsFraction() {
         return Math.min(1f, Math.max(0f, mIsLeftRightSplit
-                ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
-                : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
+                ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f)
+                        / getBottomRightBounds().right
+                : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f)
+                        / getBottomRightBounds().bottom));
     }
 
     private void updateInvisibleRect() {
@@ -435,7 +455,8 @@
     }
 
     private void updateBounds(int position) {
-        updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */);
+        updateBounds(position, getTopLeftBounds(), getBottomRightBounds(), mDividerBounds,
+                true /* setEffectBounds */);
     }
 
     /** Updates recording bounds of divider window and both of the splits. */
@@ -638,8 +659,8 @@
         updateBounds(mDividerPosition);
         mWinToken1 = null;
         mWinToken2 = null;
-        mWinBounds1.setEmpty();
-        mWinBounds2.setEmpty();
+        getTopLeftContentBounds().setEmpty();
+        getBottomRightContentBounds().setEmpty();
     }
 
     /**
@@ -835,7 +856,8 @@
                 insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0;
 
         final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
-                mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position;
+                mIsLeftRightSplit ? getBottomRightBounds().width() : getBottomRightBounds().height()
+        ).position;
         final Rect endBounds1 = new Rect();
         final Rect endBounds2 = new Rect();
         final Rect endDividerBounds = new Rect();
@@ -847,12 +869,12 @@
         endBounds2.offset(-mRootBounds.left, -mRootBounds.top);
         endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
 
-        ValueAnimator animator1 = moveSurface(t, topLeftStage, getRefBounds1(), endBounds1,
+        ValueAnimator animator1 = moveSurface(t, topLeftStage, getTopLeftRefBounds(), endBounds1,
                 -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */,
                 shouldVeil);
-        ValueAnimator animator2 = moveSurface(t, bottomRightStage, getRefBounds2(), endBounds2,
-                insets.left, insets.top, true /* roundCorners */, false /* isGoingBehind */,
-                shouldVeil);
+        ValueAnimator animator2 = moveSurface(t, bottomRightStage, getBottomRightRefBounds(),
+                endBounds2, insets.left, insets.top, true /* roundCorners */,
+                false /* isGoingBehind */, shouldVeil);
         ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(),
                 endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */,
                 false /* isGoingBehind */, false /* addVeil */);
@@ -1059,10 +1081,10 @@
             // Resets layer of divider bar to make sure it is always on top.
             t.setLayer(dividerLeash, Integer.MAX_VALUE);
         }
-        getRefBounds1(mTempRect);
+        copyTopLeftRefBounds(mTempRect);
         t.setPosition(leash1, mTempRect.left, mTempRect.top)
                 .setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
-        getRefBounds2(mTempRect);
+        copyBottomRightRefBounds(mTempRect);
         t.setPosition(leash2, mTempRect.left, mTempRect.top)
                 .setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
 
@@ -1084,15 +1106,17 @@
     public boolean applyTaskChanges(WindowContainerTransaction wct,
             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
         boolean boundsChanged = false;
-        if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
-            setTaskBounds(wct, task1, mBounds1);
-            mWinBounds1.set(mBounds1);
+        if (!getTopLeftBounds().equals(getTopLeftContentBounds())
+                || !task1.token.equals(mWinToken1)) {
+            setTaskBounds(wct, task1, getTopLeftBounds());
+            getTopLeftContentBounds().set(getTopLeftBounds());
             mWinToken1 = task1.token;
             boundsChanged = true;
         }
-        if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
-            setTaskBounds(wct, task2, mBounds2);
-            mWinBounds2.set(mBounds2);
+        if (!getBottomRightBounds().equals(getBottomRightContentBounds())
+                || !task2.token.equals(mWinToken2)) {
+            setTaskBounds(wct, task2, getBottomRightBounds());
+            getBottomRightContentBounds().set(getBottomRightBounds());
             mWinToken2 = task2.token;
             boundsChanged = true;
         }
@@ -1129,22 +1153,22 @@
     public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY,
             ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
         if (offsetX == 0 && offsetY == 0) {
-            wct.setBounds(taskInfo1.token, mBounds1);
+            wct.setBounds(taskInfo1.token, getTopLeftBounds());
             wct.setScreenSizeDp(taskInfo1.token,
                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
 
-            wct.setBounds(taskInfo2.token, mBounds2);
+            wct.setBounds(taskInfo2.token, getBottomRightBounds());
             wct.setScreenSizeDp(taskInfo2.token,
                     SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
         } else {
-            getBounds1(mTempRect);
+            copyTopLeftBounds(mTempRect);
             mTempRect.offset(offsetX, offsetY);
             wct.setBounds(taskInfo1.token, mTempRect);
             wct.setScreenSizeDp(taskInfo1.token,
                     taskInfo1.configuration.screenWidthDp,
                     taskInfo1.configuration.screenHeightDp);
 
-            getBounds2(mTempRect);
+            copyBottomRightBounds(mTempRect);
             mTempRect.offset(offsetX, offsetY);
             wct.setBounds(taskInfo2.token, mTempRect);
             wct.setScreenSizeDp(taskInfo2.token,
@@ -1162,9 +1186,9 @@
         pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow);
         pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide);
         pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition);
-        pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString());
+        pw.println(innerPrefix + "bounds1=" + getTopLeftBounds().toShortString());
         pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString());
-        pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString());
+        pw.println(innerPrefix + "bounds2=" + getBottomRightBounds().toShortString());
     }
 
     /** Handles layout change event. */
@@ -1274,15 +1298,16 @@
             }
 
             final boolean topLeftShrink = isLeftRightSplit
-                    ? position < mWinBounds1.right : position < mWinBounds1.bottom;
+                    ? position < getTopLeftContentBounds().right
+                    : position < getTopLeftContentBounds().bottom;
             if (topLeftShrink) {
                 mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
-                mContentBounds.set(mWinBounds1);
-                mSurfaceBounds.set(mBounds1);
+                mContentBounds.set(getTopLeftContentBounds());
+                mSurfaceBounds.set(getTopLeftBounds());
             } else {
                 mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
-                mContentBounds.set(mWinBounds2);
-                mSurfaceBounds.set(mBounds2);
+                mContentBounds.set(getBottomRightContentBounds());
+                mSurfaceBounds.set(getBottomRightBounds());
             }
 
             if (mDismissingSide != DOCKED_INVALID) {
@@ -1334,12 +1359,12 @@
                     case DOCKED_TOP:
                     case DOCKED_LEFT:
                         targetLeash = leash1;
-                        mTempRect.set(mBounds1);
+                        mTempRect.set(getTopLeftBounds());
                         break;
                     case DOCKED_BOTTOM:
                     case DOCKED_RIGHT:
                         targetLeash = leash2;
-                        mTempRect.set(mBounds2);
+                        mTempRect.set(getBottomRightBounds());
                         break;
                 }
             } else if (mParallaxType == PARALLAX_ALIGN_CENTER) {
@@ -1347,12 +1372,12 @@
                     case DOCKED_TOP:
                     case DOCKED_LEFT:
                         targetLeash = leash1;
-                        mTempRect.set(mBounds1);
+                        mTempRect.set(getTopLeftBounds());
                         break;
                     case DOCKED_BOTTOM:
                     case DOCKED_RIGHT:
                         targetLeash = leash2;
-                        mTempRect.set(mBounds2);
+                        mTempRect.set(getBottomRightBounds());
                         break;
                 }
             }
@@ -1530,7 +1555,7 @@
         private int getTargetYOffset() {
             final int desireOffset = Math.abs(mEndImeTop - mStartImeTop);
             // Make sure to keep at least 30% visible for the top split.
-            final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX);
+            final int maxOffset = (int) (getTopLeftBounds().height() * ADJUSTED_SPLIT_FRACTION_MAX);
             return -Math.min(desireOffset, maxOffset);
         }
 
@@ -1580,11 +1605,11 @@
                     t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
                 }
 
-                getRefBounds1(mTempRect);
+                copyTopLeftRefBounds(mTempRect);
                 mTempRect.offset(0, mYOffsetForIme);
                 t.setPosition(leash1, mTempRect.left, mTempRect.top);
 
-                getRefBounds2(mTempRect);
+                copyBottomRightRefBounds(mTempRect);
                 mTempRect.offset(0, mYOffsetForIme);
                 t.setPosition(leash2, mTempRect.left, mTempRect.top);
                 adjusted = true;
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 02df38e..6023750 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
@@ -636,7 +636,7 @@
     static Optional<FreeformComponents> provideFreeformComponents(
             @DynamicOverride Optional<FreeformComponents> freeformComponents,
             Context context) {
-        if (FreeformComponents.isFreeformEnabled(context)) {
+        if (FreeformComponents.requiresFreeformComponents(context)) {
             return freeformComponents;
         }
         return Optional.empty();
@@ -992,7 +992,7 @@
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
         return desktopTasksController.flatMap((lazy) -> {
-            if (DesktopModeStatus.canEnterDesktopMode(context)) {
+            if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
                 return Optional.of(lazy.get());
             }
             return Optional.empty();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 96f8024..37ba63e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -286,57 +286,15 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
             IWindowManager windowManager,
-            ShellCommandHandler shellCommandHandler,
             ShellTaskOrganizer taskOrganizer,
-            @DynamicOverride DesktopRepository desktopRepository,
             DisplayController displayController,
-            ShellController shellController,
-            DisplayInsetsController displayInsetsController,
             SyncTransactionQueue syncQueue,
             Transitions transitions,
-            Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            InteractionJankMonitor interactionJankMonitor,
-            AppToWebGenericLinksParser genericLinksParser,
-            AssistContentRequester assistContentRequester,
-            MultiInstanceHelper multiInstanceHelper,
-            Optional<DesktopTasksLimiter> desktopTasksLimiter,
-            AppHandleEducationController appHandleEducationController,
-            AppToWebEducationController appToWebEducationController,
-            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
-            Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
             FocusTransitionObserver focusTransitionObserver,
-            DesktopModeEventLogger desktopModeEventLogger) {
-        if (DesktopModeStatus.canEnterDesktopMode(context)) {
-            return new DesktopModeWindowDecorViewModel(
-                    context,
-                    mainExecutor,
-                    mainHandler,
-                    mainChoreographer,
-                    bgExecutor,
-                    shellInit,
-                    shellCommandHandler,
-                    windowManager,
-                    taskOrganizer,
-                    desktopRepository,
-                    displayController,
-                    shellController,
-                    displayInsetsController,
-                    syncQueue,
-                    transitions,
-                    desktopTasksController,
-                    rootTaskDisplayAreaOrganizer,
-                    interactionJankMonitor,
-                    genericLinksParser,
-                    assistContentRequester,
-                    multiInstanceHelper,
-                    desktopTasksLimiter,
-                    appHandleEducationController,
-                    appToWebEducationController,
-                    windowDecorCaptionHandleRepository,
-                    desktopActivityOrientationHandler,
-                    focusTransitionObserver,
-                    desktopModeEventLogger);
+            Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel) {
+        if (desktopModeWindowDecorViewModel.isPresent()) {
+            return desktopModeWindowDecorViewModel.get();
         }
         return new CaptionWindowDecorViewModel(
                 context,
@@ -406,7 +364,7 @@
             Optional<TaskChangeListener> taskChangeListener) {
         // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
         //                    override for this controller from the base module
-        ShellInit init = FreeformComponents.isFreeformEnabled(context) ? shellInit : null;
+        ShellInit init = FreeformComponents.requiresFreeformComponents(context) ? shellInit : null;
         return new FreeformTaskListener(
                 context,
                 init,
@@ -847,7 +805,7 @@
             DisplayController displayController,
             ShellTaskOrganizer shellTaskOrganizer,
             ShellCommandHandler shellCommandHandler) {
-        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+        if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
             return Optional.of(
                     new DesktopImmersiveController(
                             shellInit,
@@ -908,7 +866,7 @@
             Context context,
             @ShellMainThread ShellExecutor shellExecutor,
             @ShellMainThread Handler mainHandler,
-            Choreographer mainChoreographer,
+            @ShellMainThread Choreographer mainChoreographer,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
@@ -934,7 +892,7 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger
     ) {
-        if (!DesktopModeStatus.canEnterDesktopMode(context)) {
+        if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
             return Optional.empty();
         }
         return Optional.of(new DesktopModeWindowDecorViewModel(context, shellExecutor, mainHandler,
@@ -1074,7 +1032,8 @@
             ShellInit shellInit,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer
     ) {
-        if (!DesktopModeStatus.canEnterDesktopMode(context)) {
+        if (!DesktopModeStatus.canEnterDesktopMode(context)
+                && !DesktopModeStatus.overridesShowAppHandle(context)) {
             return Optional.empty();
         }
         return Optional.of(
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 0a39076..a0bdd9f 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
@@ -70,6 +70,8 @@
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.internal.protolog.ProtoLog
 import com.android.window.flags.Flags
+import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
+import com.android.wm.shell.Flags.enableFlexibleSplit
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
@@ -102,6 +104,7 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED
 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -1512,11 +1515,15 @@
             WINDOWING_MODE_MULTI_WINDOW -> {
                 val splitPosition = splitScreenController
                     .determineNewInstancePosition(callingTaskInfo)
+                // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+                //  specific cases in the future.
+                val splitIndex = if (enableFlexibleSplit())
+                    splitScreenController.determineNewInstanceIndex(callingTaskInfo) else
+                    SPLIT_INDEX_UNDEFINED
                 splitScreenController.startIntent(
                     launchIntent, context.userId, fillIn, splitPosition,
                     options.toBundle(), null /* hideTaskToken */,
-                    true /* forceLaunchNewTask */
-                )
+                    true /* forceLaunchNewTask */, splitIndex)
             }
             WINDOWING_MODE_FREEFORM -> {
                 val wct = WindowContainerTransaction()
@@ -1661,6 +1668,13 @@
                     transition, wct, task.displayId
                 )
             }
+        } else if (taskRepository.isActiveTask(task.taskId)) {
+            // If a freeform task receives a request for a fullscreen launch, apply the same
+            // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED
+            // set when needed can interfere with future split / multi-instance transitions.
+            return WindowContainerTransaction().also { wct ->
+                addMoveToFullscreenChanges(wct, task)
+            }
         }
         return null
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
index 5d22c1e..ae9d21f62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
@@ -41,6 +41,9 @@
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP;
 import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -81,6 +84,7 @@
 import com.android.wm.shell.draganddrop.anim.TwoFiftyFiftyTargetAnimator;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.split.SplitScreenConstants;
+import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
@@ -219,8 +223,10 @@
                         displayRegion.splitHorizontally(startHitRegion, endHitRegion);
                     }
 
-                    mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds, -1));
-                    mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds,
+                            SPLIT_INDEX_0));
+                    mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds,
+                            SPLIT_INDEX_1));
                 } else {
                     // TODO(b/349828130), move this into init function and/or the insets updating
                     //  callback
@@ -287,9 +293,10 @@
                         displayRegion.splitVertically(leftHitRegion, rightHitRegion);
                     }
 
-                    mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds,
+                            SPLIT_INDEX_UNDEFINED));
                     mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds,
-                            -1));
+                            SPLIT_INDEX_UNDEFINED));
                 } else {
                     final Rect topHitRegion = new Rect();
                     final Rect bottomHitRegion = new Rect();
@@ -308,9 +315,10 @@
                         displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
                     }
 
-                    mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds,
+                            SPLIT_INDEX_UNDEFINED));
                     mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds,
-                            -1));
+                            SPLIT_INDEX_UNDEFINED));
                 }
             }
         } else {
@@ -378,9 +386,9 @@
                 ? mFullscreenStarter
                 : mSplitscreenStarter;
         if (mSession.appData != null) {
-            launchApp(mSession, starter, position, hideTaskToken);
+            launchApp(mSession, starter, position, hideTaskToken, target.index);
         } else {
-            launchIntent(mSession, starter, position, hideTaskToken);
+            launchIntent(mSession, starter, position, hideTaskToken, target.index);
         }
 
         if (enableFlexibleSplit()) {
@@ -392,9 +400,10 @@
      * Launches an app provided by SysUI.
      */
     private void launchApp(DragSession session, Starter starter, @SplitPosition int position,
-            @Nullable WindowContainerToken hideTaskToken) {
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d",
-                position);
+            @Nullable WindowContainerToken hideTaskToken, @SplitIndex int splitIndex) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                "Launching app data at position=%d index=%d",
+                position, splitIndex);
         final ClipDescription description = session.getClipDescription();
         final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK);
         final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT);
@@ -429,7 +438,7 @@
                 }
             }
             starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */,
-                    position, opts, hideTaskToken);
+                    position, opts, hideTaskToken, splitIndex);
         }
     }
 
@@ -437,7 +446,7 @@
      * Launches an intent sender provided by an application.
      */
     private void launchIntent(DragSession session, Starter starter, @SplitPosition int position,
-            @Nullable WindowContainerToken hideTaskToken) {
+            @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d",
                 position);
         final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic();
@@ -452,7 +461,7 @@
         final Bundle opts = baseActivityOpts.toBundle();
         starter.startIntent(session.launchableIntent,
                 session.launchableIntent.getCreatorUserHandle().getIdentifier(),
-                null /* fillIntent */, position, opts, hideTaskToken);
+                null /* fillIntent */, position, opts, hideTaskToken, index);
     }
 
     @Override
@@ -541,7 +550,7 @@
                 @Nullable Bundle options, UserHandle user);
         void startIntent(PendingIntent intent, int userId, Intent fillInIntent,
                 @SplitPosition int position, @Nullable Bundle options,
-                @Nullable WindowContainerToken hideTaskToken);
+                @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index);
         void enterSplitScreen(int taskId, boolean leftOrTop);
 
         /**
@@ -592,7 +601,7 @@
         @Override
         public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent,
                 int position, @Nullable Bundle options,
-                @Nullable WindowContainerToken hideTaskToken) {
+                @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) {
             if (hideTaskToken != null) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
                         "Default starter does not support hide task token");
@@ -641,13 +650,13 @@
         final Rect hitRegion;
         // The approximate visual region for where the task will start
         final Rect drawRegion;
-        int index;
+        @SplitIndex int index;
 
         /**
          * @param index 0-indexed, represents which position of drop target this object represents,
          *              0 to N for left to right, top to bottom
          */
-        public Target(@Type int t, Rect hit, Rect draw, int index) {
+        public Target(@Type int t, Rect hit, Rect draw, @SplitIndex int index) {
             type = t;
             hitRegion = hit;
             drawRegion = draw;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
index 9f532f5..5461952 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
@@ -22,6 +22,10 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.draganddrop.SplitDragPolicy.Target
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_2
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_3
 
 /**
  * Represents Drop Zone targets and animations for when the system is currently in a 2 app 50/50
@@ -98,7 +102,7 @@
                     farStartBounds.right + halfDividerWidth,
                     farStartBounds.bottom
                 ),
-                farStartBounds, 0
+                farStartBounds, SPLIT_INDEX_0
             )
         )
         targets.add(
@@ -110,7 +114,7 @@
                     startBounds.right + halfDividerWidth,
                     startBounds.bottom
                 ),
-                startBounds, 1
+                startBounds, SPLIT_INDEX_1
             )
         )
         targets.add(
@@ -120,7 +124,7 @@
                     endBounds.left - halfDividerWidth,
                     endBounds.top, endBounds.right, endBounds.bottom
                 ),
-                endBounds, 2
+                endBounds, SPLIT_INDEX_2
             )
         )
         targets.add(
@@ -130,7 +134,7 @@
                     farEndBounds.left - halfDividerWidth,
                     farEndBounds.top, farEndBounds.right, farEndBounds.bottom
                 ),
-                farEndBounds, 3
+                farEndBounds, SPLIT_INDEX_3
             )
         )
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
index 3379ff2..24b74c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
@@ -23,6 +23,7 @@
 import android.provider.Settings;
 
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
@@ -59,4 +60,12 @@
                 || Settings.Global.getInt(context.getContentResolver(),
                 DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
     }
+
+    /**
+     * Freeform is enabled or we need the components to enable the app handle when desktop mode is
+     * not enabled
+     */
+    public static boolean requiresFreeformComponents(Context context) {
+        return isFreeformEnabled(context) || DesktopModeStatus.overridesShowAppHandle(context);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 18f9cc7..b6d19b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -69,7 +69,7 @@
         mWindowDecorViewModel = windowDecorViewModel;
         mTaskChangeListener = taskChangeListener;
         mFocusTransitionObserver = focusTransitionObserver;
-        if (FreeformComponents.isFreeformEnabled(context)) {
+        if (FreeformComponents.requiresFreeformComponents(context)) {
             shellInit.addInitCallback(this::onInit, this);
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index 319bfac..e4f8333 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -173,6 +173,10 @@
         return mKeyguardShowing;
     }
 
+    public boolean isKeyguardAnimating() {
+        return !mStartedTransitions.isEmpty();
+    }
+
     @Override
     public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
         mDreamToken = taskInfo.getActivityType() == ACTIVITY_TYPE_DREAM ? taskInfo.token : null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 3e6d36c..39ed9ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -54,10 +54,27 @@
      */
     int STAGE_TYPE_SIDE = 1;
 
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    int STAGE_TYPE_A = 2;
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    int STAGE_TYPE_B = 3;
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    int STAGE_TYPE_C = 4;
+
     @IntDef(prefix = { "STAGE_TYPE_" }, value = {
             STAGE_TYPE_UNDEFINED,
             STAGE_TYPE_MAIN,
-            STAGE_TYPE_SIDE
+            STAGE_TYPE_SIDE,
+            // Used for flexible split
+            STAGE_TYPE_A,
+            STAGE_TYPE_B,
+            STAGE_TYPE_C
     })
     @interface StageType {}
 
@@ -128,6 +145,9 @@
             case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
             case STAGE_TYPE_MAIN: return "MAIN";
             case STAGE_TYPE_SIDE: return "SIDE";
+            case STAGE_TYPE_A: return "STAGE_A";
+            case STAGE_TYPE_B: return "STAGE_B";
+            case STAGE_TYPE_C: return "STAGE_C";
             default: return "UNKNOWN(" + stage + ")";
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 6398d31..4f0f676 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -24,6 +24,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
 import static com.android.wm.shell.common.MultiInstanceHelper.getComponent;
 import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent;
 import static com.android.wm.shell.common.MultiInstanceHelper.samePackage;
@@ -33,6 +34,9 @@
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -98,6 +102,7 @@
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
+import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
@@ -325,7 +330,6 @@
     /**
      * @return an Array of RunningTaskInfo's ordered by leftToRight or topTopBottom
      */
-    @Nullable
     public ActivityManager.RunningTaskInfo[] getAllTaskInfos() {
         // TODO(b/349828130) Add the third stage task info and not rely on positions
         ActivityManager.RunningTaskInfo topLeftTask = getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
@@ -335,7 +339,7 @@
             return new ActivityManager.RunningTaskInfo[]{topLeftTask, bottomRightTask};
         }
 
-        return null;
+        return new ActivityManager.RunningTaskInfo[0];
     }
 
     /** Check task is under split or not by taskId. */
@@ -405,7 +409,7 @@
     public void prepareEnterSplitScreen(WindowContainerTransaction wct,
             ActivityManager.RunningTaskInfo taskInfo, int startPosition) {
         mStageCoordinator.prepareEnterSplitScreen(wct, taskInfo, startPosition,
-                false /* resizeAnim */);
+                false /* resizeAnim */, SPLIT_INDEX_UNDEFINED);
     }
 
     /**
@@ -451,6 +455,24 @@
         }
     }
 
+    /**
+     * Determines which split index a new instance of a task should take.
+     * @param callingTask The task requesting a new instance.
+     * @return the split index of the new instance
+     */
+    @SplitIndex
+    public int determineNewInstanceIndex(@NonNull ActivityManager.RunningTaskInfo callingTask) {
+        if (!enableFlexibleSplit()) {
+            throw new IllegalStateException("Use determineNewInstancePosition");
+        }
+        if (callingTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                || getSplitPosition(callingTask.taskId) == SPLIT_POSITION_TOP_OR_LEFT) {
+            return SPLIT_INDEX_1;
+        } else {
+            return SPLIT_INDEX_0;
+        }
+    }
+
     public void enterSplitScreen(int taskId, boolean leftOrTop) {
         enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction());
     }
@@ -685,7 +707,10 @@
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntentWithInstanceId: reason=%d",
                 ENTER_REASON_LAUNCHER);
         mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
-        startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */);
+        // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+        //  specific cases in the future. Only focusing on parity with starting intent/task
+        startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */,
+                SPLIT_INDEX_UNDEFINED);
     }
 
     private void startIntentAndTask(PendingIntent pendingIntent, int userId1,
@@ -775,9 +800,9 @@
     @Override
     public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options,
-            @Nullable WindowContainerToken hideTaskToken) {
+            @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) {
         startIntent(intent, userId1, fillInIntent, position, options, hideTaskToken,
-                false /* forceLaunchNewTask */);
+                false /* forceLaunchNewTask */, index);
     }
 
     /**
@@ -790,7 +815,8 @@
      */
     public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options,
-            @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask) {
+            @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask,
+            @SplitIndex int index) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                 "startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1,
                 fillInIntent, position);
@@ -816,7 +842,7 @@
         if (taskInfo != null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                     "Found suitable background task=%s", taskInfo);
-            mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken);
+            mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken, index);
 
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Start task in background");
             return;
@@ -841,7 +867,8 @@
             }
         }
 
-        mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken);
+        mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken,
+                index);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 84004941..3091be5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -21,6 +21,7 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS;
 import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN;
@@ -55,6 +56,9 @@
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /** Manages transition animations for split-screen. */
@@ -268,22 +272,21 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback,
-            @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
-            @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
+            @NonNull Map<WindowContainerToken, SplitDecorManager> rootDecorMap) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "playResizeAnimation: transition=%d", info.getDebugId());
         initTransition(transition, finishTransaction, finishCallback);
 
+        Set<WindowContainerToken> rootDecorKeys = rootDecorMap.keySet();
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
-            if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) {
+            if (rootDecorKeys.contains(change.getContainer())) {
                 final SurfaceControl leash = change.getLeash();
                 startTransaction.setPosition(leash, change.getEndAbsBounds().left,
                         change.getEndAbsBounds().top);
                 startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(),
                         change.getEndAbsBounds().height());
 
-                SplitDecorManager decor = mainRoot.equals(change.getContainer())
-                        ? mainDecor : sideDecor;
+                SplitDecorManager decor = rootDecorMap.get(change.getContainer());
 
                 // This is to ensure onFinished be called after all animations ended.
                 ValueAnimator va = new ValueAnimator();
@@ -433,15 +436,22 @@
             Transitions.TransitionHandler handler,
             @Nullable TransitionConsumedCallback consumedCallback,
             @Nullable TransitionFinishedCallback finishCallback,
-            @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
+            @Nullable SplitDecorManager mainDecor, @Nullable SplitDecorManager sideDecor,
+            @Nullable List<SplitDecorManager> decorManagers) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                 "  splitTransition deduced Resize split screen.");
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b",
                 mPendingResize != null);
         if (mPendingResize != null) {
             mPendingResize.cancel(null);
-            mainDecor.cancelRunningAnimations();
-            sideDecor.cancelRunningAnimations();
+            if (enableFlexibleSplit()) {
+                for (SplitDecorManager stage : decorManagers) {
+                    stage.cancelRunningAnimations();
+                }
+            } else {
+                mainDecor.cancelRunningAnimations();
+                sideDecor.cancelRunningAnimations();
+            }
             mAnimations.clear();
             onFinish(null /* wct */);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e692c61..45ecfa9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -33,6 +33,7 @@
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
 import static com.android.wm.shell.common.split.SplitScreenUtils.isPartiallyOffscreen;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
@@ -43,6 +44,7 @@
 import static com.android.wm.shell.shared.TransitionUtil.isOrderOnly;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
+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_90_10;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1;
@@ -51,9 +53,12 @@
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.splitPositionToString;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_A;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_B;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
@@ -142,6 +147,7 @@
 import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.shared.split.SplitBounds;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
+import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
@@ -153,11 +159,15 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * Coordinates the staging (visibility, sizing, ...) of the split-screen stages.
@@ -178,10 +188,11 @@
     // entered
     private static final int DISABLE_LAUNCH_ADJACENT_AFTER_ENTER_TIMEOUT_MS = 1000;
 
-    private final StageTaskListener mMainStage;
-    private final StageTaskListener mSideStage;
+    private StageTaskListener mMainStage;
+    private StageTaskListener mSideStage;
     @SplitPosition
     private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
+    private StageOrderOperator mStageOrderOperator;
 
     private final int mDisplayId;
     private SplitLayout mSplitLayout;
@@ -335,22 +346,32 @@
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
 
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task");
-        mMainStage = new StageTaskListener(
-                mContext,
-                mTaskOrganizer,
-                mDisplayId,
-                this /*stageListenerCallbacks*/,
-                mSyncQueue,
-                iconProvider,
-                mWindowDecorViewModel);
-        mSideStage = new StageTaskListener(
-                mContext,
-                mTaskOrganizer,
-                mDisplayId,
-                this /*stageListenerCallbacks*/,
-                mSyncQueue,
-                iconProvider,
-                mWindowDecorViewModel);
+        if (enableFlexibleSplit()) {
+            mStageOrderOperator = new StageOrderOperator(mContext,
+                    mTaskOrganizer,
+                    mDisplayId,
+                    this /*stageListenerCallbacks*/,
+                    mSyncQueue,
+                    iconProvider,
+                    mWindowDecorViewModel);
+        } else {
+            mMainStage = new StageTaskListener(
+                    mContext,
+                    mTaskOrganizer,
+                    mDisplayId,
+                    this /*stageListenerCallbacks*/,
+                    mSyncQueue,
+                    iconProvider,
+                    mWindowDecorViewModel, STAGE_TYPE_MAIN);
+            mSideStage = new StageTaskListener(
+                    mContext,
+                    mTaskOrganizer,
+                    mDisplayId,
+                    this /*stageListenerCallbacks*/,
+                    mSyncQueue,
+                    iconProvider,
+                    mWindowDecorViewModel, STAGE_TYPE_SIDE);
+        }
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -422,24 +443,63 @@
     }
 
     public boolean isSplitScreenVisible() {
-        return mSideStage.mVisible && mMainStage.mVisible;
+        if (enableFlexibleSplit()) {
+            return runForActiveStagesAllMatch((stage) -> stage.mVisible);
+        } else {
+            return mSideStage.mVisible && mMainStage.mVisible;
+        }
     }
 
-    private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) {
-        mMainStage.activate(wct, includingTopTask);
+    /**
+     * @param includingTopTask reparents the current top task into the stage defined by index
+     *                         (or mainStage in legacy split)
+     * @param index the index to move the current visible task into, if undefined will arbitrarily
+     *              choose a stage to launch into
+     */
+    private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask,
+            int index) {
+        if (enableFlexibleSplit()) {
+            mStageOrderOperator.onEnteringSplit(SNAP_TO_2_50_50);
+            if (index == SPLIT_INDEX_UNDEFINED || !includingTopTask) {
+                // If we aren't includingTopTask, then the call to activate on the stage is
+                // effectively a no-op. Previously the stage kept track of the "isActive" state,
+                // but now that gets set in the "onEnteringSplit" call above.
+                //
+                // index == UNDEFINED case might change, but as of now no use case where we activate
+                // without an index specified.
+                return;
+            }
+            @SplitIndex int oppositeIndex = index == SPLIT_INDEX_0 ? SPLIT_INDEX_1 : SPLIT_INDEX_0;
+            StageTaskListener activatingStage = mStageOrderOperator.getStageForIndex(oppositeIndex);
+            activatingStage.activate(wct, includingTopTask);
+        } else {
+            mMainStage.activate(wct, includingTopTask);
+        }
     }
 
     public boolean isSplitActive() {
-        return mMainStage.isActive();
+        if (enableFlexibleSplit()) {
+            return mStageOrderOperator.isActive();
+        } else {
+            return mMainStage.isActive();
+        }
     }
 
     /**
      * Deactivates main stage by removing the stage from the top level split root (usually when a
      * task underneath gets removed from the stage root).
-     * @param reparentToTop whether we want to put the stage root back on top
+     * @param stageToTop stage which we want to put on top
      */
-    private void deactivateSplit(WindowContainerTransaction wct, boolean reparentToTop) {
-        mMainStage.deactivate(wct, reparentToTop);
+    private void deactivateSplit(WindowContainerTransaction wct, @StageType int stageToTop) {
+        if (enableFlexibleSplit()) {
+            StageTaskListener stageToDeactivate = mStageOrderOperator.getAllStages().stream()
+                    .filter(stage -> stage.getId() == stageToTop)
+                    .findFirst().orElseThrow();
+            stageToDeactivate.deactivate(wct, true /*toTop*/);
+            mStageOrderOperator.onExitingSplit();
+        } else {
+            mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
+        }
     }
 
     /** @return whether this transition-request has the launch-adjacent flag. */
@@ -463,11 +523,12 @@
         // If one of the splitting tasks support auto-pip, wm-core might reparent the task to TDA
         // and file a TRANSIT_PIP transition when finishing transitions.
         // @see com.android.server.wm.RootWindowContainer#moveActivityToPinnedRootTask
-        if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
-            return true;
+        if (enableFlexibleSplit()) {
+            return mStageOrderOperator.getActiveStages().stream()
+                    .anyMatch(stage -> stage.getChildCount() == 0);
+        } else {
+            return mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0;
         }
-
-        return false;
     }
 
     /** Checks if `transition` is a pending enter-split transition. */
@@ -477,10 +538,19 @@
 
     @StageType
     int getStageOfTask(int taskId) {
-        if (mMainStage.containsTask(taskId)) {
-            return STAGE_TYPE_MAIN;
-        } else if (mSideStage.containsTask(taskId)) {
-            return STAGE_TYPE_SIDE;
+        if (enableFlexibleSplit()) {
+            StageTaskListener stageTaskListener = mStageOrderOperator.getActiveStages().stream()
+                    .filter(stage -> stage.containsTask(taskId))
+                    .findFirst().orElse(null);
+            if (stageTaskListener != null) {
+                return stageTaskListener.getId();
+            }
+        } else {
+            if (mMainStage.containsTask(taskId)) {
+                return STAGE_TYPE_MAIN;
+            } else if (mSideStage.containsTask(taskId)) {
+                return STAGE_TYPE_SIDE;
+            }
         }
 
         return STAGE_TYPE_UNDEFINED;
@@ -490,14 +560,22 @@
         if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskId) {
             return true;
         }
-        return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId);
+        if (enableFlexibleSplit()) {
+            return mStageOrderOperator.getActiveStages().stream()
+                    .anyMatch((stage) -> stage.isRootTaskId(taskId));
+        } else {
+            return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId);
+        }
     }
 
     boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition,
             WindowContainerTransaction wct) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "moveToStage: task=%d position=%d", task.taskId,
                 stagePosition);
-        prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */);
+        // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+        //  specific cases in the future. Only focusing on parity with starting intent/task
+        prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */,
+                SPLIT_INDEX_UNDEFINED);
         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct,
                 null, this,
                 isSplitScreenVisible()
@@ -595,11 +673,14 @@
      *                      same window container transaction as the starting of the intent.
      */
     void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options,
-            @Nullable WindowContainerToken hideTaskToken) {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d", taskId, position);
+            @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) {
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d index=%d",
+                taskId, position, index);
         mSplitRequest = new SplitRequest(taskId, position);
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+        options = enableFlexibleSplit()
+                ? resolveStartStageForIndex(options, null /*wct*/, index)
+                : resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
         if (hideTaskToken != null) {
             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom");
             wct.reorder(hideTaskToken, false /* onTop */);
@@ -623,7 +704,7 @@
         // If split screen is not activated, we're expecting to open a pair of apps to split.
         final int extraTransitType = isSplitActive()
                 ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
-        prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering);
+        prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index);
 
         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this,
                 extraTransitType, !mIsDropEntering);
@@ -635,13 +716,16 @@
      *                      same window container transaction as the starting of the intent.
      */
     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
-            @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken) {
+            @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken,
+            @SplitIndex int index) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntent: intent=%s position=%d", intent.getIntent(),
                 position);
         mSplitRequest = new SplitRequest(intent.getIntent(), position);
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+        options = enableFlexibleSplit()
+                ? resolveStartStageForIndex(options, null /*wct*/, index)
+                : resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
         if (hideTaskToken != null) {
             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom");
             wct.reorder(hideTaskToken, false /* onTop */);
@@ -666,13 +750,17 @@
         // If split screen is not activated, we're expecting to open a pair of apps to split.
         final int extraTransitType = isSplitActive()
                 ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
-        prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering);
+        prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index);
 
         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this,
                 extraTransitType, !mIsDropEntering);
     }
 
-    /** Starts 2 tasks in one transition. */
+    /**
+     * Starts 2 tasks in one transition.
+     * @param taskId1 starts in the mSideStage
+     * @param taskId2 starts in the mainStage #startWithTask()
+     */
     void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2,
             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
@@ -687,11 +775,19 @@
 
         setSideStagePosition(splitPosition, wct);
         options1 = options1 != null ? options1 : new Bundle();
-        addActivityOptions(options1, mSideStage);
+        StageTaskListener stageForTask1;
+        if (enableFlexibleSplit()) {
+            stageForTask1 = mStageOrderOperator.getStageForLegacyPosition(splitPosition,
+                    true /*checkAllStagesIfNotActive*/);
+        } else {
+            stageForTask1 = mSideStage;
+        }
+        addActivityOptions(options1, stageForTask1);
         prepareTasksForSplitScreen(new int[] {taskId1, taskId2}, wct);
         wct.startTask(taskId1, options1);
 
-        startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId);
+        startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId,
+                splitPosition);
     }
 
     /** Start an intent and a task to a split pair in one transition. */
@@ -721,7 +817,8 @@
         wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
         prepareTasksForSplitScreen(new int[] {taskId}, wct);
 
-        startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId);
+        startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId,
+                splitPosition);
     }
 
     /**
@@ -765,7 +862,8 @@
         wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
         prepareTasksForSplitScreen(new int[] {taskId}, wct);
 
-        startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId);
+        startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId,
+                splitPosition);
     }
 
     /**
@@ -795,11 +893,14 @@
      */
     private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
             @Nullable Bundle mainOptions, @PersistentSnapPosition int snapPosition,
-            @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+            @Nullable RemoteTransition remoteTransition, InstanceId instanceId,
+            @SplitPosition int splitPosition) {
         if (!isSplitActive()) {
             // Build a request WCT that will launch both apps such that task 0 is on the main stage
             // while task 1 is on the side stage.
-            activateSplit(wct, false /* reparentToTop */);
+            // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+            //  specific cases in the future. Only focusing on parity with starting intent/task
+            activateSplit(wct, false /* reparentToTop */, SPLIT_INDEX_UNDEFINED);
         }
         mSplitLayout.setDivideRatio(snapPosition);
         updateWindowBounds(mSplitLayout, wct);
@@ -807,10 +908,19 @@
         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
                 false /* reparentLeafTaskIfRelaunch */);
         setRootForceTranslucent(false, wct);
-
+        // All callers of this method set the correct activity options on mSideStage,
+        // so we choose the opposite stage for this method
+        StageTaskListener stage;
+        if (enableFlexibleSplit()) {
+            stage = mStageOrderOperator
+                    .getStageForLegacyPosition(reverseSplitPosition(splitPosition),
+                            false /*checkAllStagesIfNotActive*/);
+        } else {
+            stage = mMainStage;
+        }
         // Make sure the launch options will put tasks in the corresponding split roots
         mainOptions = mainOptions != null ? mainOptions : new Bundle();
-        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(mainOptions, stage);
 
         // Add task launch requests
         wct.startTask(mainTaskId, mainOptions);
@@ -866,7 +976,9 @@
         if (!isSplitActive()) {
             // Build a request WCT that will launch both apps such that task 0 is on the main stage
             // while task 1 is on the side stage.
-            activateSplit(wct, false /* reparentToTop */);
+            // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+            //  specific cases in the future. Only focusing on parity with starting intent/task
+            activateSplit(wct, false /* reparentToTop */, SPLIT_INDEX_UNDEFINED);
         }
 
         setSideStagePosition(splitPosition, wct);
@@ -974,6 +1086,31 @@
         mSideStage.evictInvisibleChildren(wct);
     }
 
+    /**
+     * @param index for the new stage that will be opening. Ex. if app is dragged to
+     *              index=1, then this will tell the stage at index=1 to launch the task
+     *              in the wct in that stage. This doesn't verify that the non-specified
+     *              indices' stages have their tasks correctly set/re-parented.
+     */
+    Bundle resolveStartStageForIndex(@Nullable Bundle options,
+            @Nullable WindowContainerTransaction wct,
+            @SplitIndex int index) {
+        StageTaskListener oppositeStage;
+        if (index == SPLIT_INDEX_UNDEFINED) {
+            // Arbitrarily choose a stage
+            oppositeStage = mStageOrderOperator.getStageForIndex(SPLIT_INDEX_1);
+        } else {
+            oppositeStage = mStageOrderOperator.getStageForIndex(index);
+        }
+        if (options == null) {
+            options = new Bundle();
+        }
+        updateStageWindowBoundsForIndex(wct, index);
+        addActivityOptions(options, oppositeStage);
+
+        return options;
+    }
+
     Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
             @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
         switch (stage) {
@@ -1041,20 +1178,35 @@
             return INVALID_TASK_ID;
         }
 
-        return mSideStagePosition == splitPosition
-                ? mSideStage.getTopVisibleChildTaskId()
-                : mMainStage.getTopVisibleChildTaskId();
+        if (enableFlexibleSplit()) {
+            StageTaskListener stage = mStageOrderOperator.getStageForLegacyPosition(splitPosition,
+                    true /*checkAllStagesIfNotActive*/);
+            return stage != null ? stage.getTopVisibleChildTaskId() : INVALID_TASK_ID;
+        } else {
+            return mSideStagePosition == splitPosition
+                    ? mSideStage.getTopVisibleChildTaskId()
+                    : mMainStage.getTopVisibleChildTaskId();
+        }
     }
 
     void switchSplitPosition(String reason) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "switchSplitPosition");
         final SurfaceControl.Transaction t = mTransactionPool.acquire();
         mTempRect1.setEmpty();
-        final StageTaskListener topLeftStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
-        final StageTaskListener bottomRightStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
-
+        final StageTaskListener topLeftStage;
+        final StageTaskListener bottomRightStage;
+        if (enableFlexibleSplit()) {
+            topLeftStage = mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT,
+                            false /*checkAllStagesIfNotActive*/);
+            bottomRightStage = mStageOrderOperator
+                    .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                    false /*checkAllStagesIfNotActive*/);
+        } else {
+            topLeftStage =
+                    mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+            bottomRightStage =
+                    mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        }
         // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the
         // parent of all 3 leaves). We don't want the user to be able to tap and focus a window
         // while it is moving across the screen, because granting focus also recalculates the
@@ -1091,9 +1243,13 @@
                 });
 
         ProtoLog.v(WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
-        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                mSplitLayout.isLeftRightSplit());
+        if (enableFlexibleSplit()) {
+            // TODO(b/374825718) update logging for 2+ apps
+        } else {
+            mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLeftRightSplit());
+        }
     }
 
     void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -1101,8 +1257,26 @@
         if (mSideStagePosition == sideStagePosition) return;
         mSideStagePosition = sideStagePosition;
         sendOnStagePositionChanged();
+        StageTaskListener stage = enableFlexibleSplit()
+                ? mStageOrderOperator.getStageForLegacyPosition(mSideStagePosition,
+                true /*checkAllStagesIfNotActive*/)
+                : mSideStage;
 
-        if (mSideStage.mVisible) {
+        if (stage.mVisible) {
+            if (wct == null) {
+                // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
+                onLayoutSizeChanged(mSplitLayout);
+            } else {
+                updateWindowBounds(mSplitLayout, wct);
+                sendOnBoundsChanged();
+            }
+        }
+    }
+
+    private void updateStageWindowBoundsForIndex(@Nullable WindowContainerTransaction wct,
+            @SplitIndex int index) {
+        StageTaskListener stage = mStageOrderOperator.getStageForIndex(index);
+        if (stage.mVisible) {
             if (wct == null) {
                 // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
                 onLayoutSizeChanged(mSplitLayout);
@@ -1152,10 +1326,17 @@
     void recordLastActiveStage() {
         if (!isSplitActive() || !isSplitScreenVisible()) {
             mLastActiveStage = STAGE_TYPE_UNDEFINED;
-        } else if (mMainStage.isFocused()) {
-            mLastActiveStage = STAGE_TYPE_MAIN;
-        } else if (mSideStage.isFocused()) {
-            mLastActiveStage = STAGE_TYPE_SIDE;
+        } else if (enableFlexibleSplit()) {
+            mStageOrderOperator.getActiveStages().stream()
+                    .filter(StageTaskListener::isFocused)
+                    .findFirst()
+                    .ifPresent(stage -> mLastActiveStage = stage.getId());
+        } else {
+            if (mMainStage.isFocused()) {
+                mLastActiveStage = STAGE_TYPE_MAIN;
+            } else if (mSideStage.isFocused()) {
+                mLastActiveStage = STAGE_TYPE_SIDE;
+            }
         }
     }
 
@@ -1212,7 +1393,7 @@
         mSplitLayout.getInvisibleBounds(mTempRect1);
         if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) {
             mSideStage.removeAllTasks(wct, false /* toTop */);
-            deactivateSplit(wct, false /* reparentToTop */);
+            deactivateSplit(wct, STAGE_TYPE_UNDEFINED);
             wct.reorder(mRootTaskInfo.token, false /* onTop */);
             setRootForceTranslucent(true, wct);
             wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
@@ -1241,7 +1422,7 @@
                 childrenToTop.fadeOutDecor(() -> {
                     WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
                     mIsExiting = false;
-                    deactivateSplit(finishedWCT, childrenToTop == mMainStage /* reparentToTop */);
+                    deactivateSplit(finishedWCT, childrenToTop.getId());
                     mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
                     setRootForceTranslucent(true, finishedWCT);
@@ -1369,8 +1550,13 @@
         mRecentTasks.ifPresent(recentTasks -> {
             // Notify recents if we are exiting in a way that breaks the pair, and disable further
             // updates to splits in the recents until we enter split again
-            mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
-            mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
+            if (enableFlexibleSplit()) {
+                runForActiveStages((stage) ->
+                        stage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)));
+            } else {
+                mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
+                mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
+            }
         });
         logExit(exitReason);
     }
@@ -1383,15 +1569,22 @@
     void prepareExitSplitScreen(@StageType int stageToTop,
             @NonNull WindowContainerTransaction wct) {
         if (!isSplitActive()) return;
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%d", stageToTop);
-        mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
-        deactivateSplit(wct, stageToTop == STAGE_TYPE_MAIN);
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s",
+                stageTypeToString(stageToTop));
+        if (enableFlexibleSplit()) {
+            mStageOrderOperator.getActiveStages().stream()
+                    .filter(stage -> stage.getId() != stageToTop)
+                    .forEach(stage -> stage.removeAllTasks(wct, false /*toTop*/));
+        } else {
+            mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
+        }
+        deactivateSplit(wct, stageToTop);
     }
 
     private void prepareEnterSplitScreen(WindowContainerTransaction wct) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen");
         prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED,
-                !mIsDropEntering);
+                !mIsDropEntering, SPLIT_INDEX_UNDEFINED);
     }
 
     /**
@@ -1400,7 +1593,7 @@
      */
     void prepareEnterSplitScreen(WindowContainerTransaction wct,
             @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
-            boolean resizeAnim) {
+            boolean resizeAnim, @SplitIndex int index) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen: position=%d resize=%b",
                 startPosition, resizeAnim);
         onSplitScreenEnter();
@@ -1412,7 +1605,7 @@
         if (isSplitActive()) {
             prepareBringSplit(wct, taskInfo, startPosition, resizeAnim);
         } else {
-            prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim);
+            prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim, index);
         }
     }
 
@@ -1433,14 +1626,22 @@
             if (!mSkipEvictingMainStageChildren) {
                 mMainStage.evictAllChildren(wct);
             }
-            mMainStage.reparentTopTask(wct);
+            // TODO(b/349828130) revisit bring split from BG to FG scenarios
+            if (enableFlexibleSplit()) {
+                runForActiveStages(stage -> stage.reparentTopTask(wct));
+            } else {
+                mMainStage.reparentTopTask(wct);
+            }
             prepareSplitLayout(wct, resizeAnim);
         }
     }
 
+    /**
+     * @param index The index that has already been assigned a stage
+     */
     private void prepareActiveSplit(WindowContainerTransaction wct,
             @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
-            boolean resizeAnim) {
+            boolean resizeAnim, @SplitIndex int index) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareActiveSplit: task=%d isSplitVisible=%b",
                 taskInfo != null ? taskInfo.taskId : -1, isSplitScreenVisible());
         // We handle split visibility itself on shell transition, but sometimes we didn't
@@ -1450,7 +1651,7 @@
             setSideStagePosition(startPosition, wct);
             mSideStage.addTask(taskInfo, wct);
         }
-        activateSplit(wct, true /* reparentToTop */);
+        activateSplit(wct, true /* reparentToTop */, index);
         prepareSplitLayout(wct, resizeAnim);
     }
 
@@ -1477,8 +1678,13 @@
     void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen");
         mSplitLayout.update(null, true /* resetImePosition */);
-        mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash);
-        mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash);
+        if (enableFlexibleSplit()) {
+            runForActiveStages((stage) ->
+                    stage.getSplitDecorManager().inflate(mContext, stage.mRootLeash));
+        } else {
+            mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash);
+            mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash);
+        }
         setDividerVisibility(true, finishT);
         // Ensure divider surface are re-parented back into the hierarchy at the end of the
         // transition. See Transition#buildFinishTransaction for more detail.
@@ -1492,6 +1698,10 @@
         mSplitRequest = null;
         updateRecentTasksSplitPair();
 
+        if (enableFlexibleSplit()) {
+            // TODO(b/374825718) log 2+ apps
+            return;
+        }
         mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
                 getMainStagePosition(), mMainStage.getTopChildTaskUid(),
                 getSideStagePosition(), mSideStage.getTopChildTaskUid(),
@@ -1499,8 +1709,17 @@
     }
 
     void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
-        outTopOrLeftBounds.set(mSplitLayout.getBounds1());
-        outBottomOrRightBounds.set(mSplitLayout.getBounds2());
+        outTopOrLeftBounds.set(mSplitLayout.getTopLeftBounds());
+        outBottomOrRightBounds.set(mSplitLayout.getBottomRightBounds());
+    }
+
+    private void runForActiveStages(Consumer<StageTaskListener> consumer) {
+        mStageOrderOperator.getActiveStages().forEach(consumer);
+    }
+
+    private boolean runForActiveStagesAllMatch(Predicate<StageTaskListener> predicate) {
+        List<StageTaskListener> activeStages = mStageOrderOperator.getActiveStages();
+        return !activeStages.isEmpty() && activeStages.stream().allMatch(predicate);
     }
 
     @SplitPosition
@@ -1516,6 +1735,9 @@
     private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) {
         ActivityOptions options = ActivityOptions.fromBundle(opts);
         if (launchTarget != null) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+                    "addActivityOptions setting launch root for stage=%s",
+                    stageTypeToString(launchTarget.getId()));
             options.setLaunchRootTask(launchTarget.mRootTaskInfo.token);
         }
         // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
@@ -1559,8 +1781,15 @@
             listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(),
                     getSideStageBounds());
         }
-        mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
-        mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+        if (enableFlexibleSplit()) {
+            // TODO(b/349828130) replace w/ stageID
+            mStageOrderOperator.getAllStages().forEach(
+                    stage -> stage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_UNDEFINED)
+            );
+        } else {
+            mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
+            mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+        }
     }
 
     private void sendOnStagePositionChanged() {
@@ -1584,17 +1813,25 @@
             boolean present, boolean visible) {
         int stage;
         if (present) {
-            stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+            if (enableFlexibleSplit()) {
+                stage = stageListener.getId();
+            } else {
+                stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+            }
         } else {
             // No longer on any stage
             stage = STAGE_TYPE_UNDEFINED;
         }
-        if (stage == STAGE_TYPE_MAIN) {
-            mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                    mSplitLayout.isLeftRightSplit());
-        } else if (stage == STAGE_TYPE_SIDE) {
-            mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                    mSplitLayout.isLeftRightSplit());
+        if (!enableFlexibleSplit()) {
+            if (stage == STAGE_TYPE_MAIN) {
+                mLogger.logMainStageAppChange(getMainStagePosition(),
+                        mMainStage.getTopChildTaskUid(),
+                        mSplitLayout.isLeftRightSplit());
+            } else if (stage == STAGE_TYPE_SIDE) {
+                mLogger.logSideStageAppChange(getSideStagePosition(),
+                        mSideStage.getTopChildTaskUid(),
+                        mSplitLayout.isLeftRightSplit());
+            }
         }
         if (present) {
             updateRecentTasksSplitPair();
@@ -1620,19 +1857,40 @@
             return;
         }
         mRecentTasks.ifPresent(recentTasks -> {
-            Rect topLeftBounds = mSplitLayout.getBounds1();
-            Rect bottomRightBounds = mSplitLayout.getBounds2();
-            int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId();
-            int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId();
+            Rect topLeftBounds = new Rect();
+            mSplitLayout.copyTopLeftBounds(topLeftBounds);
+            Rect bottomRightBounds = new Rect();
+            mSplitLayout.copyBottomRightBounds(bottomRightBounds);
+
+            int sideStageTopTaskId;
+            int mainStageTopTaskId;
+            if (enableFlexibleSplit()) {
+                List<StageTaskListener> activeStages = mStageOrderOperator.getActiveStages();
+                if (activeStages.size() != 2) {
+                    sideStageTopTaskId = mainStageTopTaskId = INVALID_TASK_ID;
+                } else {
+                    // doesn't matter which one we assign to? What matters is the order of 0 and 1?
+                    mainStageTopTaskId = activeStages.get(0).getTopVisibleChildTaskId();
+                    sideStageTopTaskId = activeStages.get(1).getTopVisibleChildTaskId();
+                }
+            } else {
+                mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId();
+                sideStageTopTaskId= mSideStage.getTopVisibleChildTaskId();
+            }
             boolean sideStageTopLeft = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
             int leftTopTaskId;
             int rightBottomTaskId;
-            if (sideStageTopLeft) {
-                leftTopTaskId = sideStageTopTaskId;
-                rightBottomTaskId = mainStageTopTaskId;
-            } else {
+            if (enableFlexibleSplit()) {
                 leftTopTaskId = mainStageTopTaskId;
                 rightBottomTaskId = sideStageTopTaskId;
+            } else {
+                if (sideStageTopLeft) {
+                    leftTopTaskId = sideStageTopTaskId;
+                    rightBottomTaskId = mainStageTopTaskId;
+                } else {
+                    leftTopTaskId = mainStageTopTaskId;
+                    rightBottomTaskId = sideStageTopTaskId;
+                }
             }
 
             if (Flags.enableFlexibleTwoAppSplit()) {
@@ -1741,29 +1999,59 @@
     @VisibleForTesting
     @Override
     public void onRootTaskAppeared() {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b",
-                mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask);
+        if (enableFlexibleSplit()) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s",
+                mRootTaskInfo);
+            mStageOrderOperator.getAllStages().forEach(stage -> {
+                ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+                        "    onRootStageAppeared stageId=%s hasRoot=%b",
+                        stageTypeToString(stage.getId()), stage.mHasRootTask);
+            });
+        } else {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+                    "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b",
+                    mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask);
+        }
+        boolean notAllStagesHaveRootTask;
+        if (enableFlexibleSplit()) {
+            notAllStagesHaveRootTask = mStageOrderOperator.getAllStages().stream()
+                    .anyMatch((stage) -> !stage.mHasRootTask);
+        } else {
+            notAllStagesHaveRootTask = !mMainStage.mHasRootTask
+                    || !mSideStage.mHasRootTask;
+        }
         // Wait unit all root tasks appeared.
-        if (mRootTaskInfo == null
-                || !mMainStage.mHasRootTask
-                || !mSideStage.mHasRootTask) {
+        if (mRootTaskInfo == null || notAllStagesHaveRootTask) {
             return;
         }
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
-        wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
+        if (enableFlexibleSplit()) {
+            mStageOrderOperator.getAllStages().forEach(stage ->
+                    wct.reparent(stage.mRootTaskInfo.token, mRootTaskInfo.token, true));
+        } else {
+            wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
+            wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
+        }
 
-        // Make the stages adjacent to each other so they occlude what's behind them.
-        wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
         setRootForceTranslucent(true, wct);
-        mSplitLayout.getInvisibleBounds(mTempRect1);
-        wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+        if (!enableFlexibleSplit()) {
+            //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+
+            // Make the stages adjacent to each other so they occlude what's behind them.
+            wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+            mSplitLayout.getInvisibleBounds(mTempRect1);
+            wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+        }
         mSyncQueue.queue(wct);
-        mSyncQueue.runInSync(t -> {
-            t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
-        });
-        mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
+        if (!enableFlexibleSplit()) {
+            mSyncQueue.runInSync(t -> {
+                t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
+            });
+            mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
+        } else {
+            // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+        }
     }
 
     @Override
@@ -1958,15 +2246,21 @@
     }
 
     @Override
-    public void onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason) {
+    public void onSnappedToDismiss(boolean closedBottomRightStage, @ExitReason int exitReason) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSnappedToDismiss: bottomOrRight=%b reason=%s",
-                bottomOrRight, exitReasonToString(exitReason));
-        final boolean mainStageToTop =
-                bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
+                closedBottomRightStage, exitReasonToString(exitReason));
+        boolean mainStageToTop =
+                closedBottomRightStage ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
                         : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
-        final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
-
-        final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+        StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
+        int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+        if (enableFlexibleSplit()) {
+            toTopStage = mStageOrderOperator.getStageForLegacyPosition(closedBottomRightStage
+                            ? SPLIT_POSITION_TOP_OR_LEFT
+                            : SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                    false /*checkAllStagesIfNotActive*/);
+            dismissTop = toTopStage.getId();
+        }
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         toTopStage.resetBounds(wct);
         prepareExitSplitScreen(dismissTop, wct);
@@ -1998,8 +2292,21 @@
         updateSurfaceBounds(layout, t, shouldUseParallaxEffect);
         getMainStageBounds(mTempRect1);
         getSideStageBounds(mTempRect2);
-        mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately);
-        mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately);
+        if (enableFlexibleSplit()) {
+            StageTaskListener ltStage =
+                    mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT,
+                    false /*checkAllStagesIfNotActive*/);
+            StageTaskListener brStage =
+                    mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                            false /*checkAllStagesIfNotActive*/);
+            ltStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately);
+            brStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately);
+        } else {
+            mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY,
+                    mShowDecorImmediately);
+            mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY,
+                    mShowDecorImmediately);
+        }
         t.apply();
         mTransactionPool.release(t);
     }
@@ -2015,19 +2322,33 @@
         if (!sizeChanged) {
             // We still need to resize on decor for ensure all current status clear.
             final SurfaceControl.Transaction t = mTransactionPool.acquire();
-            mMainStage.onResized(t);
-            mSideStage.onResized(t);
+            if (enableFlexibleSplit()) {
+                runForActiveStages(stage -> stage.onResized(t));
+            } else {
+                mMainStage.onResized(t);
+                mSideStage.onResized(t);
+            }
             mTransactionPool.release(t);
             return;
         }
-
+        List<SplitDecorManager> decorManagers = new ArrayList<>();
+        SplitDecorManager mainDecor = null;
+        SplitDecorManager sideDecor = null;
+        if (enableFlexibleSplit()) {
+            decorManagers = mStageOrderOperator.getActiveStages().stream()
+                    .map(StageTaskListener::getSplitDecorManager)
+                    .toList();
+        } else {
+            mainDecor = mMainStage.getSplitDecorManager();
+            sideDecor = mSideStage.getSplitDecorManager();
+        }
         sendOnBoundsChanged();
         mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart");
         mSplitTransitions.startResizeTransition(wct, this, (aborted) -> {
             mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed");
         }, (finishWct, t) -> {
             mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish");
-        }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager());
+        }, mainDecor, sideDecor, decorManagers);
 
         if (Flags.enableFlexibleTwoAppSplit()) {
             switch (layout.calculateCurrentSnapPosition()) {
@@ -2054,29 +2375,55 @@
      * @return true if stage bounds actually .
      */
     private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
-        final StageTaskListener topLeftStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
-        final StageTaskListener bottomRightStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final StageTaskListener topLeftStage;
+        final StageTaskListener bottomRightStage;
+        if (enableFlexibleSplit()) {
+            topLeftStage = mStageOrderOperator
+                    .getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT,
+                            true /*checkAllStagesIfNotActive*/);
+            bottomRightStage = mStageOrderOperator
+                    .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                            true /*checkAllStagesIfNotActive*/);
+        } else {
+            topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                    ? mSideStage
+                    : mMainStage;
+            bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                    ? mMainStage
+                    : mSideStage;
+        }
         boolean updated = layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo,
                 bottomRightStage.mRootTaskInfo);
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateWindowBounds: topLeftStage=%s bottomRightStage=%s",
-                layout.getBounds1(), layout.getBounds2());
+                layout.getTopLeftBounds(), layout.getBottomRightBounds());
         return updated;
     }
 
     void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t,
             boolean applyResizingOffset) {
-        final StageTaskListener topLeftStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
-        final StageTaskListener bottomRightStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final StageTaskListener topLeftStage;
+        final StageTaskListener bottomRightStage;
+        if (enableFlexibleSplit()) {
+            topLeftStage = mStageOrderOperator
+                    .getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT,
+                            true /*checkAllStagesIfNotActive*/);
+            bottomRightStage = mStageOrderOperator
+                    .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                            true /*checkAllStagesIfNotActive*/);
+        } else {
+            topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                    ? mSideStage
+                    : mMainStage;
+            bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                    ? mMainStage
+                    : mSideStage;
+        }
         (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
                 bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer,
                 applyResizingOffset);
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
                 "updateSurfaceBounds: topLeftStage=%s bottomRightStage=%s",
-                layout.getBounds1(), layout.getBounds2());
+                layout.getTopLeftBounds(), layout.getBottomRightBounds());
     }
 
     @Override
@@ -2085,10 +2432,22 @@
             return SPLIT_POSITION_UNDEFINED;
         }
 
-        if (mMainStage.containsToken(token)) {
-            return getMainStagePosition();
-        } else if (mSideStage.containsToken(token)) {
-            return getSideStagePosition();
+        if (enableFlexibleSplit()) {
+            // We could migrate to/return the new INDEX enums here since most callers just care that
+            // this value isn't SPLIT_POSITION_UNDEFINED, but
+            // ImePositionProcessor#getImeTargetPosition actually uses the leftTop/bottomRight value
+            StageTaskListener stageForToken = mStageOrderOperator.getAllStages().stream()
+                    .filter(stage -> stage.containsToken(token))
+                    .findFirst().orElse(null);
+            return stageForToken == null
+                    ? SPLIT_POSITION_UNDEFINED
+                    : mStageOrderOperator.getLegacyPositionForStage(stageForToken);
+        } else {
+            if (mMainStage.containsToken(token)) {
+                return getMainStagePosition();
+            } else if (mSideStage.containsToken(token)) {
+                return getSideStagePosition();
+            }
         }
 
         return SPLIT_POSITION_UNDEFINED;
@@ -2185,27 +2544,49 @@
 
     private Rect getSideStageBounds() {
         return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
-                ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
+                ? mSplitLayout.getTopLeftBounds() : mSplitLayout.getBottomRightBounds();
     }
 
     private Rect getMainStageBounds() {
         return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
-                ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
+                ? mSplitLayout.getBottomRightBounds() : mSplitLayout.getTopLeftBounds();
     }
 
+    /**
+     * TODO(b/349828130) Currently the way this is being used is only to to get the bottomRight
+     *  stage. Eventually we'll need to rename and for now we'll repurpose the method to return
+     *  the bottomRight bounds under the flex split flag
+     */
     private void getSideStageBounds(Rect rect) {
-        if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
-            mSplitLayout.getBounds1(rect);
+        if (enableFlexibleSplit()) {
+            // Split Layout doesn't actually keep track of the bounds based on the stage,
+            // it only knows that bounds1 is leftTop position and bounds2 is bottomRight position
+            // We'll then assume this method is to get bounds of bottomRight stage
+            mSplitLayout.copyBottomRightBounds(rect);
+        }  else if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
+            mSplitLayout.copyTopLeftBounds(rect);
         } else {
-            mSplitLayout.getBounds2(rect);
+            mSplitLayout.copyBottomRightBounds(rect);
         }
     }
 
+    /**
+     * TODO(b/349828130) Currently the way this is being used is only to to get the leftTop
+     *  stage. Eventually we'll need to rename and for now we'll repurpose the method to return
+     *  the leftTop bounds under the flex split flag
+     */
     private void getMainStageBounds(Rect rect) {
-        if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
-            mSplitLayout.getBounds2(rect);
+        if (enableFlexibleSplit()) {
+            // Split Layout doesn't actually keep track of the bounds based on the stage,
+            // it only knows that bounds1 is leftTop position and bounds2 is bottomRight position
+            // We'll then assume this method is to get bounds of topLeft stage
+            mSplitLayout.copyTopLeftBounds(rect);
         } else {
-            mSplitLayout.getBounds1(rect);
+            if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                mSplitLayout.copyBottomRightBounds(rect);
+            } else {
+                mSplitLayout.copyTopLeftBounds(rect);
+            }
         }
     }
 
@@ -2214,14 +2595,23 @@
      * this task (yet) so this can also be used to identify which stage to put a task into.
      */
     private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
-        // TODO(b/184679596): Find a way to either include task-org information in the transition,
-        //                    or synchronize task-org callbacks so we can use stage.containsTask
-        if (mMainStage.mRootTaskInfo != null
-                && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
-            return mMainStage;
-        } else if (mSideStage.mRootTaskInfo != null
-                && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
-            return mSideStage;
+        if (enableFlexibleSplit()) {
+            return mStageOrderOperator.getActiveStages().stream()
+                    .filter((stage) -> stage.mRootTaskInfo != null &&
+                            taskInfo.parentTaskId == stage.mRootTaskInfo.taskId
+                    )
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            // TODO(b/184679596): Find a way to either include task-org information in the
+            //  transition, or synchronize task-org callbacks so we can use stage.containsTask
+            if (mMainStage.mRootTaskInfo != null
+                    && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
+                return mMainStage;
+            } else if (mSideStage.mRootTaskInfo != null
+                    && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
+                return mSideStage;
+            }
         }
         return null;
     }
@@ -2229,7 +2619,11 @@
     @StageType
     private int getStageType(StageTaskListener stage) {
         if (stage == null) return STAGE_TYPE_UNDEFINED;
-        return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+        if (enableFlexibleSplit()) {
+            return stage.getId();
+        } else {
+            return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+        }
     }
 
     @Override
@@ -2276,11 +2670,17 @@
         if (isSplitActive()) {
             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d split active",
                     request.getDebugId());
+            StageTaskListener primaryStage = enableFlexibleSplit()
+                    ? mStageOrderOperator.getActiveStages().get(0)
+                    : mMainStage;
+            StageTaskListener secondaryStage = enableFlexibleSplit()
+                    ? mStageOrderOperator.getActiveStages().get(1)
+                    : mSideStage;
             // Try to handle everything while in split-screen, so return a WCT even if it's empty.
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  split is active so using split"
                             + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
                             + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
-                    mMainStage.getChildCount(), mSideStage.getChildCount());
+                    primaryStage.getChildCount(), secondaryStage.getChildCount());
             out = new WindowContainerTransaction();
             if (stage != null) {
                 if (isClosingType(type) && stage.getChildCount() == 1) {
@@ -2320,11 +2720,21 @@
                     // the remote handler.
                     return null;
                 }
-
-                if ((mMainStage.containsTask(triggerTask.taskId)
-                            && mMainStage.getChildCount() == 1)
-                        || (mSideStage.containsTask(triggerTask.taskId)
-                            && mSideStage.getChildCount() == 1)) {
+                boolean anyStageContainsSingleFullscreenTask;
+                if (enableFlexibleSplit()) {
+                    anyStageContainsSingleFullscreenTask =
+                            mStageOrderOperator.getActiveStages().stream()
+                                    .anyMatch(stageListener ->
+                                            stageListener.containsTask(triggerTask.taskId)
+                                                    && stageListener.getChildCount() == 1);
+                } else {
+                    anyStageContainsSingleFullscreenTask =
+                            (mMainStage.containsTask(triggerTask.taskId)
+                                    && mMainStage.getChildCount() == 1)
+                                    || (mSideStage.containsTask(triggerTask.taskId)
+                                    && mSideStage.getChildCount() == 1);
+                }
+                if (anyStageContainsSingleFullscreenTask) {
                     // A splitting task is opening to fullscreen causes one side of the split empty,
                     // so appends operations to exit split.
                     prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
@@ -2344,11 +2754,19 @@
                 // One of the cases above handled it
                 return out;
             } else if (isSplitScreenVisible()) {
+                boolean allStagesHaveChildren;
+                if (enableFlexibleSplit()) {
+                    allStagesHaveChildren = runForActiveStagesAllMatch(stageTaskListener ->
+                            stageTaskListener.getChildCount() != 0);
+                } else {
+                    allStagesHaveChildren = mMainStage.getChildCount() != 0
+                            && mSideStage.getChildCount() != 0;
+                }
                 // If split is visible, only defer handling this transition if it's launching
                 // adjacent while there is already a split pair -- this may trigger PIP and
                 // that should be handled by the mixed handler.
                 final boolean deferTransition = requestHasLaunchAdjacentFlag(request)
-                    && mMainStage.getChildCount() != 0 && mSideStage.getChildCount() != 0;
+                    && allStagesHaveChildren;
                 return !deferTransition ? out : null;
             }
             // Don't intercept the transition if we are not handling it as a part of one of the
@@ -2588,8 +3006,15 @@
             }
 
             final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
-            if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0
-                    || dismissStages.size() == 1) {
+            boolean anyStageHasNoChildren;
+            if (enableFlexibleSplit()) {
+                anyStageHasNoChildren = mStageOrderOperator.getActiveStages().stream()
+                        .anyMatch(stage -> stage.getChildCount() == 0);
+            } else {
+                anyStageHasNoChildren = mMainStage.getChildCount() == 0
+                        || mSideStage.getChildCount() == 0;
+            }
+            if (anyStageHasNoChildren || dismissStages.size() == 1) {
                 // If the size of dismissStages == 1, one of the task is closed without prepare
                 // pending transition, which could happen if all activities were finished after
                 // finish top activity in a task, so the trigger task is null when handleRequest.
@@ -2735,24 +3160,48 @@
             shouldAnimate = startPendingDismissAnimation(
                     dismiss, info, startTransaction, finishTransaction);
             if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
-                final StageTaskListener toTopStage =
-                        dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
+                StageTaskListener toTopStage;
+                if (enableFlexibleSplit()) {
+                    toTopStage = mStageOrderOperator.getAllStages().stream()
+                            .filter(stage -> stage.getId() == dismiss.mDismissTop)
+                            .findFirst().orElseThrow();
+                } else {
+                    toTopStage = dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
+                }
                 mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction,
                         finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token,
                         toTopStage.getSplitDecorManager(), mRootTaskInfo.token);
                 return true;
             }
         } else if (mSplitTransitions.isPendingResize(transition)) {
+            Map<WindowContainerToken, SplitDecorManager> tokenDecorMap = new HashMap<>();
+            if (enableFlexibleSplit()) {
+                runForActiveStages(stageTaskListener ->
+                        tokenDecorMap.put(stageTaskListener.mRootTaskInfo.getToken(),
+                                stageTaskListener.getSplitDecorManager()));
+            } else {
+                tokenDecorMap.put(mMainStage.mRootTaskInfo.getToken(),
+                        mMainStage.getSplitDecorManager());
+                tokenDecorMap.put(mSideStage.mRootTaskInfo.getToken(),
+                        mSideStage.getSplitDecorManager());
+            }
             mSplitTransitions.playResizeAnimation(transition, info, startTransaction,
-                    finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
-                    mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(),
-                    mSideStage.getSplitDecorManager());
+                    finishTransaction, finishCallback, tokenDecorMap);
             return true;
         }
         if (!shouldAnimate) return false;
 
+        WindowContainerToken mainToken;
+        WindowContainerToken sideToken;
+        if (enableFlexibleSplit()) {
+            mainToken = mStageOrderOperator.getActiveStages().get(0).mRootTaskInfo.token;
+            sideToken = mStageOrderOperator.getActiveStages().get(1).mRootTaskInfo.token;
+        } else {
+            mainToken = mMainStage.mRootTaskInfo.token;
+            sideToken = mSideStage.mRootTaskInfo.token;
+        }
         mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
-                finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token,
+                finishCallback, mainToken, sideToken,
                 mRootTaskInfo.token);
         return true;
     }
@@ -2777,6 +3226,8 @@
         // First, verify that we actually have opened apps in both splits.
         TransitionInfo.Change mainChild = null;
         TransitionInfo.Change sideChild = null;
+        StageTaskListener firstAppStage = null;
+        StageTaskListener secondAppStage = null;
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         for (int iC = 0; iC < info.getChanges().size(); ++iC) {
             final TransitionInfo.Change change = info.getChanges().get(iC);
@@ -2785,14 +3236,19 @@
             if (mPausingTasks.contains(taskInfo.taskId)) {
                 continue;
             }
-            final @StageType int stageType = getStageType(getStageOfTask(taskInfo));
-            if (mainChild == null && stageType == STAGE_TYPE_MAIN
+            StageTaskListener stage = getStageOfTask(taskInfo);
+            final @StageType int stageType = getStageType(stage);
+            if (mainChild == null
+                    && stageType == (enableFlexibleSplit() ? STAGE_TYPE_A : STAGE_TYPE_MAIN)
                     && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
                 // Includes TRANSIT_CHANGE to cover reparenting top-most task to split.
                 mainChild = change;
-            } else if (sideChild == null && stageType == STAGE_TYPE_SIDE
+                firstAppStage = getStageOfTask(taskInfo);
+            } else if (sideChild == null
+                    && stageType == (enableFlexibleSplit() ? STAGE_TYPE_B : STAGE_TYPE_SIDE)
                     && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
                 sideChild = change;
+                secondAppStage = stage;
             } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) {
                 // Collect all to back task's and evict them when transition finished.
                 evictWct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
@@ -2848,9 +3304,9 @@
         // TODO(b/184679596): Find a way to either include task-org information in
         //                    the transition, or synchronize task-org callbacks.
         final boolean mainNotContainOpenTask =
-                mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId);
+                mainChild != null && !firstAppStage.containsTask(mainChild.getTaskInfo().taskId);
         final boolean sideNotContainOpenTask =
-                sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId);
+                sideChild != null && !secondAppStage.containsTask(sideChild.getTaskInfo().taskId);
         if (mainNotContainOpenTask) {
             Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
                     + " to have been called with " + mainChild.getTaskInfo().taskId
@@ -2863,6 +3319,8 @@
         }
         final TransitionInfo.Change finalMainChild = mainChild;
         final TransitionInfo.Change finalSideChild = sideChild;
+        final StageTaskListener finalFirstAppStage = firstAppStage;
+        final StageTaskListener finalSecondAppStage = secondAppStage;
         enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
             if (!enterTransition.mResizeAnim) {
                 // If resizing, we'll call notify at the end of the resizing animation (below)
@@ -2870,16 +3328,18 @@
             }
             if (finalMainChild != null) {
                 if (!mainNotContainOpenTask) {
-                    mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
+                    finalFirstAppStage.evictOtherChildren(callbackWct,
+                            finalMainChild.getTaskInfo().taskId);
                 } else {
-                    mMainStage.evictInvisibleChildren(callbackWct);
+                    finalFirstAppStage.evictInvisibleChildren(callbackWct);
                 }
             }
             if (finalSideChild != null) {
                 if (!sideNotContainOpenTask) {
-                    mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId);
+                    finalSecondAppStage.evictOtherChildren(callbackWct,
+                            finalSideChild.getTaskInfo().taskId);
                 } else {
-                    mSideStage.evictInvisibleChildren(callbackWct);
+                    finalSecondAppStage.evictInvisibleChildren(callbackWct);
                 }
             }
             if (!evictWct.isEmpty()) {
@@ -2961,8 +3421,10 @@
     public void onPipExpandToSplit(WindowContainerTransaction wct,
             ActivityManager.RunningTaskInfo taskInfo) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onPipExpandToSplit: task=%s", taskInfo);
+        // TODO(b/349828130) currently pass in index_undefined until we can revisit these
+        //  flex split + pip interactions in the future
         prepareEnterSplitScreen(wct, taskInfo, getActivateSplitPosition(taskInfo),
-                false /*resizeAnim*/);
+                false /*resizeAnim*/, SPLIT_INDEX_UNDEFINED);
 
         if (!isSplitScreenVisible() || mSplitRequest == null) {
             return;
@@ -3067,13 +3529,28 @@
         // Wait until after animation to update divider
 
         // Reset crops so they don't interfere with subsequent launches
-        t.setCrop(mMainStage.mRootLeash, null);
-        t.setCrop(mSideStage.mRootLeash, null);
+        if (enableFlexibleSplit()) {
+            runForActiveStages(stage -> t.setCrop(stage.mRootLeash, null /*crop*/));
+        } else {
+            t.setCrop(mMainStage.mRootLeash, null);
+            t.setCrop(mSideStage.mRootLeash, null);
+        }
         // Hide the non-top stage and set the top one to the fullscreen position.
         if (toStage != STAGE_TYPE_UNDEFINED) {
-            t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash);
-            t.setPosition(toStage == STAGE_TYPE_MAIN
-                    ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0);
+            if (enableFlexibleSplit()) {
+                StageTaskListener stageToKeep = mStageOrderOperator.getAllStages().stream()
+                        .filter(stage -> stage.getId() == toStage)
+                        .findFirst().orElseThrow();
+                List<StageTaskListener> stagesToHide = mStageOrderOperator.getAllStages().stream()
+                        .filter(stage -> stage.getId() != toStage)
+                        .toList();
+                stagesToHide.forEach(stage -> t.hide(stage.mRootLeash));
+                t.setPosition(stageToKeep.mRootLeash, 0, 0);
+            } else {
+                t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash);
+                t.setPosition(toStage == STAGE_TYPE_MAIN
+                        ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0);
+            }
         } else {
             for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) {
                 finishT.hide(dismissingTasks.valueAt(i));
@@ -3088,8 +3565,12 @@
 
         // Hide divider and dim layer on transition finished.
         setDividerVisibility(false, t);
-        finishT.hide(mMainStage.mDimLayer);
-        finishT.hide(mSideStage.mDimLayer);
+        if (enableFlexibleSplit()) {
+            runForActiveStages(stage -> finishT.hide(stage.mRootLeash));
+        } else {
+            finishT.hide(mMainStage.mDimLayer);
+            finishT.hide(mSideStage.mDimLayer);
+        }
     }
 
     private boolean startPendingDismissAnimation(
@@ -3110,8 +3591,12 @@
             return false;
         }
         dismissTransition.setFinishedCallback((callbackWct, callbackT) -> {
-            mMainStage.getSplitDecorManager().release(callbackT);
-            mSideStage.getSplitDecorManager().release(callbackT);
+            if (enableFlexibleSplit()) {
+                runForActiveStages(stage -> stage.getSplitDecorManager().release(callbackT));
+            } else {
+                mMainStage.getSplitDecorManager().release(callbackT);
+                mSideStage.getSplitDecorManager().release(callbackT);
+            }
             callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
         });
         return true;
@@ -3128,8 +3613,15 @@
                 if (TransitionUtil.isClosingType(change.getMode())
                         && change.getTaskInfo() != null) {
                     final int taskId = change.getTaskInfo().taskId;
-                    if (mMainStage.getTopVisibleChildTaskId() == taskId
-                            || mSideStage.getTopVisibleChildTaskId() == taskId) {
+                    boolean anyStagesHaveTask;
+                    if (enableFlexibleSplit()) {
+                        anyStagesHaveTask = mStageOrderOperator.getActiveStages().stream()
+                                .anyMatch(stage -> stage.getTopVisibleChildTaskId() == taskId);
+                    } else {
+                        anyStagesHaveTask = mMainStage.getTopVisibleChildTaskId() == taskId
+                                || mSideStage.getTopVisibleChildTaskId() == taskId;
+                    }
+                    if (anyStagesHaveTask) {
                         mPausingTasks.add(taskId);
                     }
                 }
@@ -3161,9 +3653,16 @@
             final WindowContainerTransaction.HierarchyOp op =
                     finishWct.getHierarchyOps().get(i);
             final IBinder container = op.getContainer();
+            boolean anyStageContainsContainer;
+            if (enableFlexibleSplit()) {
+                anyStageContainsContainer = mStageOrderOperator.getActiveStages().stream()
+                        .anyMatch(stage -> stage.containsContainer(container));
+            } else {
+                anyStageContainsContainer = mMainStage.containsContainer(container)
+                        || mSideStage.containsContainer(container);
+            }
             if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
-                    && (mMainStage.containsContainer(container)
-                    || mSideStage.containsContainer(container))) {
+                    && anyStageContainsContainer) {
                 updateSurfaceBounds(mSplitLayout, finishT,
                         false /* applyResizingOffset */);
                 finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
@@ -3190,10 +3689,18 @@
         // user entering recents.
         for (int i = mPausingTasks.size() - 1; i >= 0; --i) {
             final int taskId = mPausingTasks.get(i);
-            if (mMainStage.containsTask(taskId)) {
-                mMainStage.evictChild(finishWct, taskId, "recentsPairToPair");
-            } else if (mSideStage.containsTask(taskId)) {
-                mSideStage.evictChild(finishWct, taskId, "recentsPairToPair");
+            if (enableFlexibleSplit()) {
+                mStageOrderOperator.getActiveStages().stream()
+                        .filter(stage -> stage.containsTask(taskId))
+                        .findFirst()
+                        .ifPresent(stageToEvict ->
+                            stageToEvict.evictChild(finishWct, taskId, "recentsPairToPair"));
+            } else {
+                if (mMainStage.containsTask(taskId)) {
+                    mMainStage.evictChild(finishWct, taskId, "recentsPairToPair");
+                } else if (mSideStage.containsTask(taskId)) {
+                    mSideStage.evictChild(finishWct, taskId, "recentsPairToPair");
+                }
             }
         }
         // If pending enter hasn't consumed, the mix handler will invoke start pending
@@ -3256,8 +3763,15 @@
      */
     private void setSplitsVisible(boolean visible) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setSplitsVisible: visible=%b", visible);
-        mMainStage.mVisible = mSideStage.mVisible = visible;
-        mMainStage.mHasChildren = mSideStage.mHasChildren = visible;
+        if (enableFlexibleSplit()) {
+            runForActiveStages(stage -> {
+                stage.mVisible = visible;
+                stage.mHasChildren = visible;
+            });
+        } else {
+            mMainStage.mVisible = mSideStage.mVisible = visible;
+            mMainStage.mHasChildren = mSideStage.mHasChildren = visible;
+        }
     }
 
     /**
@@ -3300,6 +3814,10 @@
      * executed.
      */
     private void logExitToStage(@ExitReason int exitReason, boolean toMainStage) {
+        if (enableFlexibleSplit()) {
+            // TODO(b/374825718) update logging for 2+ apps
+            return;
+        }
         mLogger.logExit(exitReason,
                 toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
                 toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt
new file mode 100644
index 0000000..b7b3c9b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.splitscreen
+
+import android.content.Context
+import com.android.internal.protolog.ProtoLog
+import com.android.launcher3.icons.IconProvider
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.split.SplitScreenConstants
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_2
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
+import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition
+import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex
+import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition
+import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_A
+import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_B
+import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_C
+import com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString
+import com.android.wm.shell.windowdecor.WindowDecorViewModel
+import java.util.Optional
+
+/**
+ * Responsible for creating [StageTaskListener]s and maintaining their ordering on screen.
+ * Must be notified whenever stages positions change via swapping or starting/ending tasks
+ */
+class StageOrderOperator (
+        context: Context,
+        taskOrganizer: ShellTaskOrganizer,
+        displayId: Int,
+        stageCallbacks: StageTaskListener.StageListenerCallbacks,
+        syncQueue: SyncTransactionQueue,
+        iconProvider: IconProvider,
+        windowDecorViewModel: Optional<WindowDecorViewModel>
+    ) {
+
+    private val MAX_STAGES = 3
+    /**
+     * This somewhat acts as a replacement to stageTypes in the intermediary, so we want to start
+     * it after the @StageType constant values just to be safe and avoid potentially subtle bugs.
+     */
+    private var stageIds = listOf(STAGE_TYPE_A, STAGE_TYPE_B, STAGE_TYPE_C)
+
+    /**
+     * Active Stages, this list represent the current, ordered list of stages that are
+     * currently visible to the user. This map should be empty if the user is currently
+     * not in split screen. Note that this is different than if split screen is visible, which
+     * is determined by [StageListenerImpl.mVisible].
+     * Split stages can be active and in the background
+     */
+    val activeStages = mutableListOf<StageTaskListener>()
+    val allStages = mutableListOf<StageTaskListener>()
+    var isActive: Boolean = false
+    var isVisible: Boolean = false
+    @SnapPosition private var currentLayout: Int = SNAP_TO_NONE
+
+    init {
+        for(i in 0 until MAX_STAGES) {
+            allStages.add(StageTaskListener(context,
+                taskOrganizer,
+                displayId,
+                stageCallbacks,
+                syncQueue,
+                iconProvider,
+                windowDecorViewModel,
+                stageIds[i])
+            )
+        }
+    }
+
+    /**
+     * Updates internal state to keep record of "active" stages. Note that this does NOT call
+     * [StageTaskListener.activate] on the stages.
+     */
+    fun onEnteringSplit(@SnapPosition goingToLayout: Int) {
+        if (goingToLayout == currentLayout) {
+            // Add protolog here. Return for now, but maybe we want to handle swap case, TBD
+            return
+        }
+        val freeStages: List<StageTaskListener> =
+            allStages.filterNot { activeStages.contains(it) }
+        when(goingToLayout) {
+            SplitScreenConstants.SNAP_TO_2_50_50 -> {
+                if (activeStages.size < 2) {
+                    // take from allStages and add into activeStages
+                    for (i in 0 until (2 - activeStages.size)) {
+                        val stage = freeStages[i]
+                        activeStages.add(stage)
+                    }
+                }
+            }
+        }
+        ProtoLog.d(
+            ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+            "Activated stages: %d ids=%s",
+            activeStages.size,
+            activeStages.joinToString(",") { stageTypeToString(it.id) }
+        )
+        isActive = true
+    }
+
+    fun onExitingSplit() {
+        activeStages.clear()
+        isActive = false
+    }
+
+    /**
+     * Given a legacy [SplitPosition] returns one of the stages from the actives stages.
+     * If there are no active stages and [checkAllStagesIfNotActive] is not true, then will return
+     * null
+     */
+    fun getStageForLegacyPosition(@SplitPosition position: Int,
+                                  checkAllStagesIfNotActive : Boolean = false) :
+            StageTaskListener? {
+        if (activeStages.size != 2 && !checkAllStagesIfNotActive) {
+            return null
+        }
+        val listToCheck = if (activeStages.isEmpty() and checkAllStagesIfNotActive)
+            allStages else
+            activeStages
+        if (position == SPLIT_POSITION_TOP_OR_LEFT) {
+            return listToCheck[0]
+        } else if (position == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+            return listToCheck[1]
+        } else {
+            throw IllegalArgumentException("No stage for invalid position")
+        }
+    }
+
+    /**
+     * Returns a legacy split position for the given stage. If no stages are active then this will
+     * return [SPLIT_POSITION_UNDEFINED]
+     */
+    @SplitPosition
+    fun getLegacyPositionForStage(stage: StageTaskListener) : Int {
+        if (allStages[0] == stage) {
+            return SPLIT_POSITION_TOP_OR_LEFT
+        } else if (allStages[1] == stage) {
+            return SPLIT_POSITION_BOTTOM_OR_RIGHT
+        } else {
+            return SPLIT_POSITION_UNDEFINED
+        }
+    }
+
+    /**
+     * Returns the stageId from a given splitIndex. This will default to checking from all stages if
+     * [isActive] is false, otherwise will only check active stages.
+     */
+    fun getStageForIndex(@SplitIndex splitIndex: Int) : StageTaskListener {
+        // Probably should do a check for index to be w/in the bounds of the current split layout
+        // that we're currently in
+        val listToCheck = if (isActive) activeStages else allStages
+        if (splitIndex == SPLIT_INDEX_0) {
+            return listToCheck[0]
+        } else if (splitIndex == SPLIT_INDEX_1) {
+            return listToCheck[1]
+        } else if (splitIndex == SPLIT_INDEX_2) {
+            return listToCheck[2]
+        } else {
+            // Though I guess what if we're adding to the end? Maybe that indexing needs to be
+            // resolved elsewhere
+            throw IllegalStateException("No stage for the given splitIndex")
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 08cdfdb..4a37169 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -22,10 +22,12 @@
 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
+import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
 
 import android.annotation.CallSuper;
 import android.annotation.Nullable;
@@ -72,6 +74,8 @@
     // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the
     // stages should have this be set/being used
     private boolean mIsActive;
+    /** Unique identifier for this state, > 0 */
+    @StageType private final int mId;
     /** Callback interface for listening to changes in a split-screen stage. */
     public interface StageListenerCallbacks {
         void onRootTaskAppeared();
@@ -110,13 +114,14 @@
     StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
             IconProvider iconProvider,
-            Optional<WindowDecorViewModel> windowDecorViewModel) {
+            Optional<WindowDecorViewModel> windowDecorViewModel, int id) {
         mContext = context;
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
         mIconProvider = iconProvider;
         mWindowDecorViewModel = windowDecorViewModel;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
+        mId = id;
     }
 
     int getChildCount() {
@@ -161,6 +166,11 @@
         return contains(t -> t.isFocused);
     }
 
+    @StageType
+    int getId() {
+        return mId;
+    }
+
     private boolean contains(Predicate<ActivityManager.RunningTaskInfo> predicate) {
         if (mRootTaskInfo != null && predicate.test(mRootTaskInfo)) {
             return true;
@@ -197,10 +207,10 @@
     @CallSuper
     public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: taskId=%d taskParent=%d rootTask=%d "
-                        + "taskActivity=%s",
+                        + "stageId=%s taskActivity=%s",
                 taskInfo.taskId, taskInfo.parentTaskId,
                 mRootTaskInfo != null ? mRootTaskInfo.taskId : -1,
-                taskInfo.baseActivity);
+                stageTypeToString(mId), taskInfo.baseActivity);
         if (mRootTaskInfo == null) {
             mRootLeash = leash;
             mRootTaskInfo = taskInfo;
@@ -230,8 +240,9 @@
     @Override
     @CallSuper
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s",
-                taskInfo.taskId, taskInfo.baseActivity);
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s "
+                        + "stageId=%s",
+                taskInfo.taskId, taskInfo.baseActivity, stageTypeToString(mId));
         mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
         if (mRootTaskInfo.taskId == taskInfo.taskId) {
             mRootTaskInfo = taskInfo;
@@ -261,7 +272,8 @@
     @Override
     @CallSuper
     public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId);
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d stageId=%s",
+                taskInfo.taskId, stageTypeToString(mId));
         final int taskId = taskInfo.taskId;
         mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo));
         if (mRootTaskInfo.taskId == taskId) {
@@ -466,14 +478,17 @@
     }
 
     void activate(WindowContainerTransaction wct, boolean includingTopTask) {
-        if (mIsActive) return;
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b",
-                includingTopTask);
+        if (mIsActive && !enableFlexibleSplit()) return;
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b stage=%s",
+                includingTopTask, stageTypeToString(mId));
 
         if (includingTopTask) {
             reparentTopTask(wct);
         }
 
+        if (enableFlexibleSplit()) {
+            return;
+        }
         mIsActive = true;
     }
 
@@ -481,11 +496,14 @@
         deactivate(wct, false /* toTop */);
     }
 
-    void deactivate(WindowContainerTransaction wct, boolean toTop) {
-        if (!mIsActive) return;
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: toTop=%b rootTaskInfo=%s",
-                toTop, mRootTaskInfo);
-        mIsActive = false;
+    void deactivate(WindowContainerTransaction wct, boolean reparentTasksToTop) {
+        if (!mIsActive && !enableFlexibleSplit()) return;
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: reparentTasksToTop=%b "
+                        + "rootTaskInfo=%s stage=%s",
+                reparentTasksToTop, mRootTaskInfo, stageTypeToString(mId));
+        if (!enableFlexibleSplit()) {
+            mIsActive = false;
+        }
 
         if (mRootTaskInfo == null) return;
         final WindowContainerToken rootToken = mRootTaskInfo.token;
@@ -494,14 +512,15 @@
                 null /* newParent */,
                 null /* windowingModes */,
                 null /* activityTypes */,
-                toTop);
+                reparentTasksToTop);
     }
 
     // --------
-    // Previously only used in SideStage
+    // Previously only used in SideStage. With flexible split this is called for all stages
     boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b",
-                mChildrenTaskInfo.size(), toTop);
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b "
+                        + " stageI=%s",
+                mChildrenTaskInfo.size(), toTop, stageTypeToString(mId));
         if (mChildrenTaskInfo.size() == 0) return false;
         wct.reparentTasks(
                 mRootTaskInfo.token,
@@ -522,6 +541,15 @@
     }
 
     @Override
+    public String toString() {
+        return "mId: " + stageTypeToString(mId)
+                + " mVisible: " + mVisible
+                + " mActive: " + mIsActive
+                + " mHasRootTask: " + mHasRootTask
+                + " childSize: " + mChildrenTaskInfo.size();
+    }
+
+    @Override
     @CallSuper
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 17483dd..92d1f9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -370,7 +370,8 @@
         if (mRecentsHandler != null) {
             if (mSplitHandler.isSplitScreenVisible()) {
                 return this::setRecentsTransitionDuringSplit;
-            } else if (mKeyguardHandler.isKeyguardShowing()) {
+            } else if (mKeyguardHandler.isKeyguardShowing()
+                    && !mKeyguardHandler.isKeyguardAnimating()) {
                 return this::setRecentsTransitionDuringKeyguard;
             } else if (mDesktopTasksController != null
                     // Check on the default display. Recents/gesture nav is only available there
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index fd4d568..8cdbe26 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -117,6 +117,11 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during"
                 + " Keyguard #%d", info.getDebugId());
 
+        if (!mKeyguardHandler.isKeyguardShowing() || mKeyguardHandler.isKeyguardAnimating()) {
+            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Cancel mixed transition because "
+                    + "keyguard state was changed #%d", info.getDebugId());
+            return false;
+        }
         if (mInfo == null) {
             mInfo = info;
             mFinishT = finishTransaction;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f89b0d1..6f33399 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -78,6 +78,7 @@
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.WindowManager;
 import android.window.DesktopModeFlags;
 import android.window.TaskSnapshot;
 import android.window.WindowContainerToken;
@@ -412,7 +413,7 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to register window manager callbacks", e);
         }
-        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+        if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext)
                 && Flags.enableDesktopWindowingAppHandleEducation()) {
             mAppHandleEducationController.setAppHandleEducationTooltipCallbacks(
                     /* appHandleTooltipClickCallback= */(taskId) -> {
@@ -957,7 +958,7 @@
         public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-            if (DesktopModeStatus.canEnterDesktopMode(mContext)
+            if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext)
                     && !taskInfo.isFreeform()) {
                 return handleNonFreeformMotionEvent(decoration, v, e);
             } else {
@@ -1483,7 +1484,15 @@
         if (isPartOfDefaultHomePackage(taskInfo)) {
             return false;
         }
-        return DesktopModeStatus.canEnterDesktopMode(mContext)
+        final boolean isOnLargeScreen = taskInfo.getConfiguration().smallestScreenWidthDp
+                >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+        if (!DesktopModeStatus.canEnterDesktopMode(mContext)
+                && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) {
+            // Devices with multiple screens may enable the app handle but it should not show on
+            // small screens
+            return false;
+        }
+        return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext)
                 && !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
                 && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
                 && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 62be2c7..456f2c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -664,6 +664,8 @@
         }
 
         private fun bindMoreActionsPill(style: MenuStyle) {
+            moreActionsPill.background.setTint(style.backgroundColor)
+
             arrayOf(
                 screenshotBtn to SHOULD_SHOW_SCREENSHOT_BUTTON,
                 newWindowBtn to shouldShowNewWindowButton,
@@ -674,7 +676,6 @@
                 val shouldShow = it.second
                 button.apply {
                     isGone = !shouldShow
-                    background.setTint(style.backgroundColor)
                     setTextColor(style.textColor)
                     compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 4bb1e7b..11a7cf8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -70,6 +70,7 @@
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
 import com.android.wm.shell.windowdecor.common.OPACITY_12
 import com.android.wm.shell.windowdecor.common.OPACITY_40
+import com.android.wm.shell.windowdecor.common.OPACITY_60
 import com.android.wm.shell.windowdecor.common.withAlpha
 import java.util.function.Supplier
 
@@ -310,8 +311,6 @@
             .desktop_mode_maximize_menu_immersive_button_fill_padding)
         private val maximizeFillPaddingDefault = context.resources.getDimensionPixelSize(R.dimen
             .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding)
-        private val maximizeFillPaddingBottom = context.resources.getDimensionPixelSize(R.dimen
-            .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom)
         private val maximizeRestoreFillPaddingVertical = context.resources.getDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_restore_button_fill_vertical_padding)
         private val maximizeRestoreFillPaddingHorizontal = context.resources.getDimensionPixelSize(
@@ -320,7 +319,7 @@
             maximizeFillPaddingDefault,
             maximizeFillPaddingDefault,
             maximizeFillPaddingDefault,
-            maximizeFillPaddingBottom
+            maximizeFillPaddingDefault
         )
         private val maximizeRestoreFillPaddingRect = Rect(
             maximizeRestoreFillPaddingHorizontal,
@@ -684,7 +683,7 @@
                     inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(),
                     semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40),
                     activeSnapSideColor = colorScheme.primary.toArgb(),
-                    inactiveStrokeColor = colorScheme.outlineVariant.toArgb(),
+                    inactiveStrokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60),
                     activeStrokeColor = colorScheme.primary.toArgb(),
                     inactiveBackgroundColor = menuBackgroundColor,
                     activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
@@ -753,7 +752,8 @@
             val activeStrokeAndFill = colorScheme.primary.toArgb()
             val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
             val activeDrawable = createMaximizeOrImmersiveButtonDrawable(
-                strokeAndFillColor = activeStrokeAndFill,
+                strokeColor = activeStrokeAndFill,
+                fillColor = activeStrokeAndFill,
                 backgroundColor = activeBackground,
                 // Add a mask with the menu background's color because the active background color is
                 // semi transparent, otherwise the transparency will reveal the stroke/fill color
@@ -770,7 +770,8 @@
                 addState(
                     StateSet.WILD_CARD,
                     createMaximizeOrImmersiveButtonDrawable(
-                        strokeAndFillColor = colorScheme.outlineVariant.toArgb(),
+                        strokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60),
+                        fillColor = colorScheme.outlineVariant.toArgb(),
                         backgroundColor = colorScheme.surfaceContainerLow.toArgb(),
                         backgroundMask = null, // not needed because the bg color is fully opaque
                         fillPadding = fillPadding,
@@ -780,7 +781,8 @@
         }
 
         private fun createMaximizeOrImmersiveButtonDrawable(
-            @ColorInt strokeAndFillColor: Int,
+            @ColorInt strokeColor: Int,
+            @ColorInt fillColor: Int,
             @ColorInt backgroundColor: Int,
             @ColorInt backgroundMask: Int?,
             fillPadding: Rect,
@@ -794,7 +796,7 @@
                     null /* inset */,
                     null /* innerRadii */
                 )
-                paint.color = strokeAndFillColor
+                paint.color = strokeColor
                 paint.style = Paint.Style.FILL
             })
             // Second layer, a mask for the next (background) layer if needed because of
@@ -829,7 +831,7 @@
                     null /* inset */,
                     null /* innerRadii */
                 )
-                paint.color = strokeAndFillColor
+                paint.color = fillColor
                 paint.style = Paint.Style.FILL
             })
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
index f7cfbfa..c5057aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
@@ -52,6 +52,7 @@
 const val OPACITY_15 = 38
 const val OPACITY_40 = 102
 const val OPACITY_55 = 140
+const val OPACITY_60 = 153
 const val OPACITY_65 = 166
 
 /**
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 65e50f8..19829e7 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -6,6 +6,7 @@
 lbill@google.com
 madym@google.com
 hwwang@google.com
+gabiyev@google.com
 chenghsiuchang@google.com
 atsjenk@google.com
 jorgegil@google.com
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index 88dc548..0cc8b0c 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -50,6 +50,7 @@
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER
 import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP
+import android.tools.flicker.config.desktopmode.Components.SIMPLE_APP
 import android.tools.flicker.extractors.ITransitionMatcher
 import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
 import android.tools.flicker.extractors.TaggedCujTransitionMatcher
@@ -444,5 +445,25 @@
                         AppWindowOnTopAtEnd(LAUNCHER),
                     ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
             )
+        val OPEN_UNLIMITED_APPS =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("OPEN_UNLIMITED_APPS"),
+                extractor =
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                                return transitions.filter { it.type == TransitionType.OPEN }
+                        }
+                    }
+                ),
+                assertions =
+                        listOf(
+                            AppWindowBecomesVisible(DESKTOP_MODE_APP),
+                            AppWindowIsVisibleAlways(SIMPLE_APP)
+                        ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+            )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenUnlimitedApps.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenUnlimitedApps.kt
new file mode 100644
index 0000000..0a39846
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenUnlimitedApps.kt
@@ -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 com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.OPEN_UNLIMITED_APPS
+import com.android.wm.shell.scenarios.OpenUnlimitedApps
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Open many apps on the device without the window limit.
+ *
+ * Assert that the desktop task limit is not triggered.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenUnlimitedApps : OpenUnlimitedApps() {
+    @ExpectedScenarios(["OPEN_UNLIMITED_APPS"])
+    @Test
+    override fun openUnlimitedApps() = super.openUnlimitedApps()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(OPEN_UNLIMITED_APPS)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
index 0f546cd..8d04749 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
@@ -49,7 +49,8 @@
 
     @Test
     open fun enterDesktopWithDrag() {
-        testApp.enterDesktopModeWithDrag(wmHelper, device)
+        // By default this method uses drag to desktop
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @After
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp
deleted file mode 100644
index 85e6a8d..0000000
--- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp
+++ /dev/null
@@ -1,38 +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 {
-    // 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_test {
-    name: "WMShellFlickerTestsMediaProjection",
-    defaults: ["WMShellFlickerTestsDefault"],
-    manifest: "AndroidManifest.xml",
-    test_config_template: "AndroidTestTemplate.xml",
-    srcs: ["src/**/*.kt"],
-    static_libs: [
-        "WMShellFlickerTestsBase",
-        "WMShellScenariosMediaProjection",
-        "WMShellTestUtils",
-    ],
-    data: ["trace_config/*"],
-}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml
deleted file mode 100644
index 74b0daf..0000000
--- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml
+++ /dev/null
@@ -1,85 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.wm.shell.flicker">
-
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
-    <!-- Read and write traces from external storage -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <!-- Allow the test to write directly to /sdcard/ -->
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <!-- Write secure settings -->
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <!-- Capture screen contents -->
-    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
-    <!-- Enable / Disable tracing !-->
-    <uses-permission android:name="android.permission.DUMP" />
-    <!-- Run layers trace -->
-    <uses-permission android:name="android.permission.HARDWARE_TEST"/>
-    <!-- Capture screen recording -->
-    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
-    <!-- Workaround grant runtime permission exception from b/152733071 -->
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
-    <uses-permission android:name="android.permission.READ_LOGS"/>
-    <!-- Force-stop test apps -->
-    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
-    <!-- Control test app's media session -->
-    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
-    <!-- ATM.removeRootTasksWithActivityTypes() -->
-    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
-    <!-- Enable bubble notification-->
-    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
-    <!-- Allow the test to connect to perfetto trace processor -->
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
-    <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
-
-    <!-- Allow the test to write directly to /sdcard/ and connect to trace processor -->
-    <application android:requestLegacyExternalStorage="true"
-                 android:networkSecurityConfig="@xml/network_security_config"
-                 android:largeHeap="true">
-        <uses-library android:name="android.test.runner"/>
-
-        <service android:name=".NotificationListener"
-                 android:exported="true"
-                 android:label="WMShellTestsNotificationListenerService"
-                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
-            <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
-            </intent-filter>
-        </service>
-
-        <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService"
-            android:foregroundServiceType="mediaProjection"
-            android:label="WMShellTestsMediaProjectionService"
-            android:enabled="true">
-        </service>
-
-        <!-- (b/197936012) Remove startup provider due to test timeout issue -->
-        <provider
-            android:name="androidx.startup.InitializationProvider"
-            android:authorities="${applicationId}.androidx-startup"
-            tools:node="remove" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.wm.shell.flicker"
-                     android:label="WindowManager Shell Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml
deleted file mode 100644
index 40dbbac..0000000
--- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}">
-    <option name="test-tag" value="FlickerTests"/>
-    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
-    <option name="isolated-storage" value="false"/>
-
-    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-        <!-- disable DeprecatedTargetSdk warning -->
-        <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
-        <!-- keeps the screen on during tests -->
-        <option name="screen-always-on" value="on"/>
-        <!-- prevents the phone from restarting -->
-        <option name="force-skip-system-props" value="true"/>
-        <!-- set WM tracing verbose level to all -->
-        <option name="run-command" value="cmd window tracing level all"/>
-        <!-- set WM tracing to frame (avoid incomplete states) -->
-        <option name="run-command" value="cmd window tracing frame"/>
-        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
-        <option name="run-command" value="pm disable com.google.android.internal.betterbug"/>
-        <!-- ensure lock screen mode is swipe -->
-        <option name="run-command" value="locksettings set-disabled false"/>
-        <!-- restart launcher to activate TAPL -->
-        <option name="run-command"
-                value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/>
-        <!-- Increase trace size: 20mb for WM and 80mb for SF -->
-        <option name="run-command" value="cmd window tracing size 20480"/>
-        <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/>
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-        <option name="test-user-token" value="%TEST_USER%"/>
-        <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
-        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
-        <option name="run-command" value="settings put system show_touches 1"/>
-        <option name="run-command" value="settings put system pointer_location 1"/>
-        <option name="teardown-command"
-                value="settings delete secure show_ime_with_hard_keyboard"/>
-        <option name="teardown-command" value="settings delete system show_touches"/>
-        <option name="teardown-command" value="settings delete system pointer_location"/>
-        <option name="teardown-command"
-                value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true"/>
-        <option name="test-file-name" value="{MODULE}.apk"/>
-        <option name="test-file-name" value="FlickerTestApp.apk"/>
-    </target_preparer>
-
-    <!-- Needed for pushing the trace config file -->
-    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="push-file"
-                key="trace_config.textproto"
-                value="/data/misc/perfetto-traces/trace_config.textproto"
-        />
-        <!--Install the content provider automatically when we push some file in sdcard folder.-->
-        <!--Needed to avoid the installation during the test suite.-->
-        <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/>
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="package" value="{PACKAGE}"/>
-        <option name="shell-timeout" value="6600s"/>
-        <option name="test-timeout" value="6000s"/>
-        <option name="hidden-api-checks" value="false"/>
-        <option name="device-listeners" value="android.device.collectors.PerfettoListener"/>
-        <!-- PerfettoListener related arguments -->
-        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/>
-        <option name="instrumentation-arg"
-                key="perfetto_config_file"
-                value="trace_config.textproto"
-        />
-        <option name="instrumentation-arg" key="per_run" value="true"/>
-        <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
-    </test>
-    <!-- Needed for pulling the collected trace config on to the host -->
-    <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.wm.shell.flicker/files"/>
-        <option name="collect-on-run-ended-only" value="true"/>
-        <option name="clean-up" value="true"/>
-    </metrics_collector>
-</configuration>
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto
deleted file mode 100644
index 9f2e497..0000000
--- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto
+++ /dev/null
@@ -1,71 +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.
-
-# proto-message: TraceConfig
-
-# Enable periodic flushing of the trace buffer into the output file.
-write_into_file: true
-
-# Writes the userspace buffer into the file every 1s.
-file_write_period_ms: 2500
-
-# See b/126487238 - we need to guarantee ordering of events.
-flush_period_ms: 30000
-
-# The trace buffers needs to be big enough to hold |file_write_period_ms| of
-# trace data. The trace buffer sizing depends on the number of trace categories
-# enabled and the device activity.
-
-# RSS events
-buffers: {
-  size_kb: 63488
-  fill_policy: RING_BUFFER
-}
-
-data_sources {
-  config {
-    name: "linux.process_stats"
-    target_buffer: 0
-    # polled per-process memory counters and process/thread names.
-    # If you don't want the polled counters, remove the "process_stats_config"
-    # section, but keep the data source itself as it still provides on-demand
-    # thread/process naming for ftrace data below.
-    process_stats_config {
-      scan_all_processes_on_start: true
-    }
-  }
-}
-
-data_sources: {
-  config {
-    name: "linux.ftrace"
-    ftrace_config {
-      ftrace_events: "ftrace/print"
-      ftrace_events: "task/task_newtask"
-      ftrace_events: "task/task_rename"
-      atrace_categories: "ss"
-      atrace_categories: "wm"
-      atrace_categories: "am"
-      atrace_categories: "aidl"
-      atrace_categories: "input"
-      atrace_categories: "binder_driver"
-      atrace_categories: "sched_process_exit"
-      atrace_apps: "com.android.server.wm.flicker.testapp"
-      atrace_apps: "com.android.systemui"
-      atrace_apps: "com.android.wm.shell.flicker.service"
-      atrace_apps: "com.google.android.apps.nexuslauncher"
-    }
-  }
-}
-
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.kt
new file mode 100644
index 0000000..2b9772d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartAppMediaProjectionFromSplitScreen
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartAppMediaProjectionFromSplitScreen]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartAppMediaProjectionFromSplitScreenTest() : StartAppMediaProjectionFromSplitScreen()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.kt
new file mode 100644
index 0000000..e92297b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartAppMediaProjectionInSplitScreen
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartAppMediaProjectionInSplitScreen]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartAppMediaProjectionInSplitScreenTest() : StartAppMediaProjectionInSplitScreen()
\ No newline at end of file
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt
similarity index 60%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt
index a321fb0..3f810759 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt
@@ -14,7 +14,14 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package com.android.wm.shell.functional
 
-/** {@hide} */
-parcelable ForensicEvent;
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartAppMediaProjection
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartAppMediaProjection]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartAppMediaProjectionTest() : StartAppMediaProjection()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.kt
new file mode 100644
index 0000000..1975cc7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartAppMediaProjectionWithExtraIntent
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartAppMediaProjectionWithExtraIntent]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartAppMediaProjectionWithExtraIntentTest : StartAppMediaProjectionWithExtraIntent()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.kt
new file mode 100644
index 0000000..943033c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartRecentAppMediaProjectionFromSplitScreen
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartRecentAppMediaProjectionFromSplitScreen]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartRecentAppMediaProjectionFromSplitScreenTest() : StartRecentAppMediaProjectionFromSplitScreen()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.kt
new file mode 100644
index 0000000..6facfd5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartRecentAppMediaProjectionInSplitScreen
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartRecentAppMediaProjectionInSplitScreen]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartRecentAppMediaProjectionInSplitScreenTest() : StartRecentAppMediaProjectionInSplitScreen()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.kt
new file mode 100644
index 0000000..bab0905
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.StartRecentAppMediaProjection
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [StartRecentAppMediaProjection]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class StartRecentAppMediaProjectionTest() : StartRecentAppMediaProjection()
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt
new file mode 100644
index 0000000..fe2c578
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session.
+ *
+ * This is for testing that the requested app is opened as expected upon selecting it from the app
+ * selector, so capture can proceed as expected.
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartAppMediaProjection {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        testApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjection(wmHelper, targetApp)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt
new file mode 100644
index 0000000..3beece8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session, while the HOST app is in
+ * split screen
+ *
+ * This is for testing that the requested app is opened as expected upon selecting it from the app
+ * selector, so capture can proceed as expected.
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartAppMediaProjectionFromSplitScreen {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val simpleApp = SimpleAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.workspace.switchToOverview().dismissAllTasks()
+
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, simpleApp, testApp, initialRotation)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, simpleApp, testApp)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjection(wmHelper, targetApp)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt
new file mode 100644
index 0000000..d3186ae
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt
@@ -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.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session, while the TARGET app is in
+ * split screen (next to the host app)
+ *
+ * This is for testing that the split pair isn't broken, and capture still proceeds as expected
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartAppMediaProjectionInSplitScreen {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.workspace.switchToOverview().dismissAllTasks()
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, targetApp, testApp, initialRotation)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjection(wmHelper, targetApp)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withWindowSurfaceAppeared(targetApp)
+            .withWindowSurfaceAppeared(testApp)
+            .waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+        targetApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt
new file mode 100644
index 0000000..0b2a1ca
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt
@@ -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.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session but launches an intent to
+ * return to the home screen, before the intent for opening the requested app to capture.
+ *
+ * This is for testing that even if a different intent interrupts the process the launching the
+ * requested capture target, the MediaProjection process isn't interrupted and the device is still
+ * interactive.
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartAppMediaProjectionWithExtraIntent {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        testApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjectionWithExtraIntent(wmHelper, targetApp)
+
+        // Check we can still interact with device after
+        tapl.workspace.switchToAllApps().getAppIcon(targetApp.appName).launch(targetApp.packageName)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withWindowSurfaceAppeared(targetApp)
+            .waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt
new file mode 100644
index 0000000..30e0e4a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session from recents.
+ *
+ * This is for testing that the app is started from recents and capture proceeds as expected.
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartRecentAppMediaProjection {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        targetApp.open()
+        testApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt
new file mode 100644
index 0000000..f1dcf1f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session from recents while the HOST
+ * app is in split screen.
+ *
+ * This is for testing that the split pair isn't broken, and capture still proceeds as expected
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartRecentAppMediaProjectionFromSplitScreen {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val simpleApp = SimpleAppHelper(instrumentation)
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.workspace.switchToOverview().dismissAllTasks()
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        targetApp.open()
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, simpleApp, testApp, initialRotation)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        // The app we want to open for PSS will be the second item in the carousel,
+        // because the first will be the app open in split screen alongside the MediaProjection app
+        testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp, recentTasksIndex = 1)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt
new file mode 100644
index 0000000..0a6992f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt
@@ -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.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper
+import com.android.wm.shell.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/**
+ * Test scenario which requests an a single-app MediaProjection session from recents while the
+ * TARGET app is in split screen (with host app).
+ *
+ * This is for testing that the split pair isn't broken, and capture still proceeds as expected
+ */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class StartRecentAppMediaProjectionInSplitScreen {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    private val initialRotation = Rotation.ROTATION_0
+    private val targetApp = CalculatorAppHelper(instrumentation)
+    private val testApp = StartMediaProjectionAppHelper(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation)
+
+    @Before
+    fun setup() {
+        tapl.workspace.switchToOverview().dismissAllTasks()
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(initialRotation.value)
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, targetApp, testApp, initialRotation)
+    }
+
+    @Test
+    open fun startMediaProjection() {
+        testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withWindowSurfaceAppeared(targetApp)
+            .withWindowSurfaceAppeared(testApp)
+            .waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+        targetApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt
index f9706969..8c2bdad 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt
@@ -17,7 +17,11 @@
 package com.android.wm.shell.flicker.utils
 
 object MediaProjectionUtils {
-    const val REQUEST_CODE: Int = 99
+    // Request code for the normal media projection request
+    const val REQUEST_CODE_NORMAL: Int = 11
+    // Request code for the media projection request which will include an extra intent to open
+    // home screen before starting requested app
+    const val REQUEST_CODE_EXTRA_INTENT: Int = 12
     const val MSG_START_FOREGROUND_DONE: Int = 1
     const val MSG_SERVICE_DESTROYED: Int = 2
     const val EXTRA_MESSENGER: String = "messenger"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 6fa3788..ce640b5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -47,6 +47,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.bubbles.BubbleData.TimeSource;
 import com.android.wm.shell.common.ShellExecutor;
@@ -102,6 +103,7 @@
 
     private BubbleData mBubbleData;
     private TestableBubblePositioner mPositioner;
+    private UiEventLoggerFake mUiEventLogger;
 
     @Mock
     private TimeSource mTimeSource;
@@ -112,8 +114,6 @@
     @Mock
     private PendingIntent mDeleteIntent;
     @Mock
-    private BubbleLogger mBubbleLogger;
-    @Mock
     private BubbleEducationController mEducationController;
     @Mock
     private ShellExecutor mMainExecutor;
@@ -196,10 +196,12 @@
                 mock(Icon.class),
                 mMainExecutor, mBgExecutor);
 
+        mUiEventLogger = new UiEventLoggerFake();
+
         mPositioner = new TestableBubblePositioner(mContext,
                 mContext.getSystemService(WindowManager.class));
-        mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, mEducationController,
-                mMainExecutor, mBgExecutor);
+        mBubbleData = new BubbleData(getContext(), new BubbleLogger(mUiEventLogger), mPositioner,
+                mEducationController, mMainExecutor, mBgExecutor);
 
         // Used by BubbleData to set lastAccessedTime
         when(mTimeSource.currentTimeMillis()).thenReturn(1000L);
@@ -297,6 +299,82 @@
     }
 
     @Test
+    public void testRemoveBubbleFromBubbleBar_notifCancelled_logEvent() {
+        mPositioner.setShowingInBubbleBar(true);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(1);
+        assertThat(mUiEventLogger.eventId(0)).isEqualTo(
+                BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_CANCELED.getId());
+    }
+
+    @Test
+    public void testRemoveBubbleFromBubbleBar_taskFinished_logEvent() {
+        mPositioner.setShowingInBubbleBar(true);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(1);
+        assertThat(mUiEventLogger.eventId(0)).isEqualTo(
+                BubbleLogger.Event.BUBBLE_BAR_BUBBLE_ACTIVITY_FINISH.getId());
+    }
+
+    @Test
+    public void testRemoveBubbleFromBubbleBar_notifBlocked_logEvent() {
+        mPositioner.setShowingInBubbleBar(true);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_BLOCKED);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(1);
+        assertThat(mUiEventLogger.eventId(0)).isEqualTo(
+                BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED.getId());
+    }
+
+    @Test
+    public void testRemoveBubbleFromBubbleBar_noLongerBubble_logEvent() {
+        mPositioner.setShowingInBubbleBar(true);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(1);
+        assertThat(mUiEventLogger.eventId(0)).isEqualTo(
+                BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED.getId());
+    }
+
+    @Test
+    public void testRemoveBubbleFromBubbleBar_addToOverflow_logEvent() {
+        mPositioner.setShowingInBubbleBar(true);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_AGED);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(1);
+        assertThat(mUiEventLogger.eventId(0)).isEqualTo(
+                BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_ADD_AGED.getId());
+    }
+
+    @Test
+    public void testRemoveBubble_notifCancelled_noLog() {
+        mPositioner.setShowingInBubbleBar(false);
+
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_BLOCKED);
+        assertThat(mUiEventLogger.numLogs()).isEqualTo(0);
+    }
+
+    @Test
     public void ifSuppress_hideFlyout() {
         // Setup
         mBubbleData.setListener(mListener);
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 936835c..5df3957 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
@@ -116,6 +116,7 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
 import com.android.wm.shell.shared.split.SplitScreenConstants
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -3158,7 +3159,7 @@
     runOpenNewWindow(task)
     verify(splitScreenController)
       .startIntent(any(), anyInt(), any(), any(),
-        optionsCaptor.capture(), anyOrNull(), eq(true)
+        optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED)
       )
     assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
       .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
@@ -3174,7 +3175,7 @@
     verify(splitScreenController)
       .startIntent(
         any(), anyInt(), any(), any(),
-        optionsCaptor.capture(), anyOrNull(), eq(true)
+        optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED)
       )
     assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
       .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
index 2cfce69..0cf15ba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
@@ -24,6 +24,7 @@
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED;
 import static com.android.wm.shell.draganddrop.DragTestUtils.createAppClipData;
 import static com.android.wm.shell.draganddrop.DragTestUtils.createIntentClipData;
 import static com.android.wm.shell.draganddrop.DragTestUtils.createTaskInfo;
@@ -226,7 +227,7 @@
 
         mPolicy.onDropped(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */);
         verify(mFullscreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_UNDEFINED), any(), any());
+                eq(SPLIT_POSITION_UNDEFINED), any(), any(), eq(SPLIT_INDEX_UNDEFINED));
     }
 
     private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) {
@@ -241,12 +242,12 @@
 
         mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any(), eq(SPLIT_INDEX_UNDEFINED));
         reset(mSplitScreenStarter);
 
         mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any());
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any(), eq(SPLIT_INDEX_UNDEFINED));
     }
 
     private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) {
@@ -261,13 +262,13 @@
 
         mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any(), eq(SPLIT_INDEX_UNDEFINED));
         reset(mSplitScreenStarter);
 
         mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_BOTTOM),
                 null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any());
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any(), eq(SPLIT_INDEX_UNDEFINED));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 5f58265..289fd2d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -36,6 +36,8 @@
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.Rational;
@@ -46,6 +48,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.MockSurfaceControlHelper;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -67,6 +70,8 @@
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentMatchers;
@@ -81,7 +86,13 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
+@DisableFlags(Flags.FLAG_ENABLE_PIP2)
 public class PipTaskOrganizerTest extends ShellTestCase {
+    @ClassRule
+    public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
     private PipTaskOrganizer mPipTaskOrganizer;
 
     @Mock private DisplayController mMockDisplayController;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 9600351..b123f4d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -42,11 +42,14 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.common.DisplayController;
@@ -74,6 +77,8 @@
 import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -88,7 +93,11 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
+@DisableFlags(Flags.FLAG_ENABLE_PIP2)
 public class PipControllerTest extends ShellTestCase {
+    @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+    @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
     private PipController mPipController;
     private ShellInit mShellInit;
     private ShellController mShellController;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 966651f..72a7a3f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -23,6 +23,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 
@@ -200,10 +201,11 @@
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+                SPLIT_INDEX_0);
 
         verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0));
         assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
                 mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
     }
@@ -220,10 +222,11 @@
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+                SPLIT_INDEX_0);
 
         verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0));
         assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
                 mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
     }
@@ -243,10 +246,11 @@
         doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt(), any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+                SPLIT_INDEX_0);
 
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
-                isNull(), isNull());
+                isNull(), isNull(), eq(SPLIT_INDEX_0));
         verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator, never()).switchSplitPosition(any());
     }
@@ -269,10 +273,11 @@
                 .findTaskInBackground(any(), anyInt(), any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+                SPLIT_INDEX_0);
         verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
-                isNull(), isNull());
+                isNull(), isNull(), eq(SPLIT_INDEX_0));
     }
 
     @Test
@@ -289,7 +294,8 @@
                 SPLIT_POSITION_BOTTOM_OR_RIGHT);
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+                SPLIT_INDEX_0);
 
         verify(mStageCoordinator).switchSplitPosition(anyString());
     }
@@ -301,7 +307,7 @@
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
-                true /* forceLaunchNewTask */);
+                true /* forceLaunchNewTask */, SPLIT_INDEX_0);
         verify(mRecentTasks, never()).findTaskInBackground(any(), anyInt(), any());
     }
 
@@ -312,7 +318,7 @@
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
-                false /* forceLaunchNewTask */);
+                false /* forceLaunchNewTask */, SPLIT_INDEX_0);
         verify(mRecentTasks).findTaskInBackground(any(), anyInt(), any());
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 66dcef6..d13a888 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -53,8 +53,8 @@
         doReturn(dividerBounds).when(out).getDividerBounds();
         doReturn(dividerBounds).when(out).getRefDividerBounds();
         doReturn(leash).when(out).getDividerLeash();
-        doReturn(bounds1).when(out).getBounds1();
-        doReturn(bounds2).when(out).getBounds2();
+        doReturn(bounds1).when(out).getTopLeftBounds();
+        doReturn(bounds2).when(out).getBottomRightBounds();
         return out;
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index ce3944a..e32cf38 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -27,6 +27,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
@@ -133,11 +134,11 @@
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
-                mIconProvider, Optional.of(mWindowDecorViewModel)));
+                mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_MAIN));
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
-                mIconProvider, Optional.of(mWindowDecorViewModel)));
+                mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_SIDE));
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
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 a252a9d..4f6f3c6 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
@@ -19,6 +19,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -140,8 +141,8 @@
                 Optional.empty()));
         mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build();
 
-        when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
-        when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
+        when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1);
+        when(mSplitLayout.getBottomRightBounds()).thenReturn(mBounds2);
         when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds);
         when(mSplitLayout.isLeftRightSplit()).thenReturn(false);
         when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true);
@@ -165,8 +166,9 @@
         final WindowContainerTransaction wct = spy(new WindowContainerTransaction());
 
         mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
+        // TODO(b/349828130) Address this once we remove index_undefined called
         verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED));
         verify(mMainStage).reparentTopTask(eq(wct));
         assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition());
         assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition());
@@ -183,8 +185,9 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
+        // TODO(b/349828130) Address this once we remove index_undefined called
         verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED));
         assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition());
         assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition());
     }
@@ -195,8 +198,9 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
+        // TODO(b/349828130) Address this once we remove index_undefined called
         verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED));
         assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition());
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 7144a1e..fe91440 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -19,6 +19,8 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
@@ -93,7 +95,8 @@
                 mCallbacks,
                 mSyncQueue,
                 mIconProvider,
-                Optional.of(mWindowDecorViewModel));
+                Optional.of(mWindowDecorViewModel),
+                STAGE_TYPE_UNDEFINED);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
         mSurfaceControl = new SurfaceControl.Builder().setName("test").build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
new file mode 100644
index 0000000..a15b611
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
+import org.mockito.quality.Strictness
+
+/**
+ * Tests of [DesktopModeWindowDecorViewModelAppHandleOnlyTest]
+ *
+ * A subset of tests from [DesktopModeWindowDecorViewModel] for when DesktopMode is not active
+ * but we still need to show AppHandle
+ * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelAppHandleOnlyTest
+ */
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@EnableFlags(Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT)
+@RunWithLooper
+class DesktopModeWindowDecorViewModelAppHandleOnlyTest :
+    DesktopModeWindowDecorViewModelTestsBase() {
+
+    @Before
+    fun setUp() {
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .spyStatic(DragPositioningCallbackUtility::class.java)
+                .startMocking()
+        doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())}
+        setUpCommon()
+    }
+
+    @Test
+    fun testWindowDecor_showAppHandle_decorCreated() {
+        val task = createTask()
+
+        setUpMockDecorationForTask(task)
+
+        onTaskOpening(task)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
+    }
+
+    @Test
+    fun testWindowDecor_dontShowAppHandle_decorNotCreated() {
+        // Simulate device that doesn't support showing app handle
+        doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(any())}
+
+        val task = createTask()
+
+        onTaskOpening(task)
+        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
+    }
+
+    @Test
+    fun testDeleteDecorationOnChangeTransitionWhenNecessary() {
+        val task = createTask()
+        val taskSurface = SurfaceControl()
+        val decoration = setUpMockDecorationForTask(task)
+
+        onTaskOpening(task, taskSurface)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
+        task.setActivityType(ACTIVITY_TYPE_UNDEFINED)
+        onTaskChanging(task, taskSurface)
+
+        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
+        verify(decoration).close()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun testDecor_invokeOpenHandleMenuCallback_openHandleMenu() {
+        val task = createTask()
+        val decor = setUpMockDecorationForTask(task)
+        val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>()
+        // Set task as gmail
+        val gmailPackageName = "com.google.android.gm"
+        val baseComponent = ComponentName(gmailPackageName, /* class */ "")
+        task.baseActivity = baseComponent
+
+        onTaskOpening(task)
+        verify(
+            mockAppHandleEducationController,
+            times(1)
+        ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any())
+        openHandleMenuCallbackCaptor.lastValue.invoke(task.taskId)
+
+        verify(decor, times(1)).createHandleMenu(anyBoolean())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun testDecorationIsNotCreatedForSystemUIActivities() {
+        val task = createTask()
+
+        // Set task as systemUI package
+        val systemUIPackageName = context.resources.getString(
+            com.android.internal.R.string.config_systemUi)
+        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        task.baseActivity = baseComponent
+
+        onTaskOpening(task)
+
+        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
+    }
+
+    @Test
+    fun testAppHandleShowsOnlyOnLargeDisplay() {
+        val task = createTask()
+        val taskSurface = SurfaceControl()
+        setUpMockDecorationForTask(task)
+        onTaskOpening(task, taskSurface)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
+
+
+        task.setOnLargeScreen(false)
+        setUpMockDecorationForTask(task)
+        onTaskChanging(task, taskSurface)
+        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
+    }
+
+    private fun createTask(
+        displayId: Int = DEFAULT_DISPLAY,
+        @WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+        activityType: Int = ACTIVITY_TYPE_STANDARD,
+        activityInfo: ActivityInfo = ActivityInfo(),
+        requestingImmersive: Boolean = false,
+        shouldShowAspectRatioButton: Boolean = true
+    ): RunningTaskInfo {
+        val task = createTask(
+            displayId, windowingMode, activityType, activityInfo, requestingImmersive)
+        task.setOnLargeScreen(shouldShowAspectRatioButton)
+        return task
+    }
+
+    private fun RunningTaskInfo.setOnLargeScreen(large: Boolean) {
+        configuration.smallestScreenWidthDp = if (large) 1000 else 100
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index ef9b30c..78fb4b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -28,31 +28,19 @@
 import android.content.Context
 import android.content.Intent
 import android.content.Intent.ACTION_MAIN
-import android.content.pm.ActivityInfo
 import android.graphics.Rect
 import android.graphics.Region
 import android.hardware.display.DisplayManager
 import android.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
 import android.net.Uri
-import android.os.Handler
 import android.os.SystemClock
-import android.os.UserHandle
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.CheckFlagsRule
-import android.platform.test.flag.junit.DeviceFlagsValueProvider
-import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
-import android.testing.TestableContext
 import android.testing.TestableLooper.RunWithLooper
-import android.util.SparseArray
-import android.view.Choreographer
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.ISystemGestureExclusionListener
-import android.view.IWindowManager
-import android.view.InputChannel
-import android.view.InputMonitor
 import android.view.InsetsSource
 import android.view.InsetsState
 import android.view.KeyEvent
@@ -69,67 +57,28 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.internal.jank.InteractionJankMonitor
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer
-import com.android.wm.shell.ShellTaskOrganizer
-import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.TestRunningTaskInfoBuilder
-import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
-import com.android.wm.shell.apptoweb.AssistContentRequester
-import com.android.wm.shell.common.DisplayChangeController
-import com.android.wm.shell.common.DisplayController
-import com.android.wm.shell.common.DisplayInsetsController
-import com.android.wm.shell.common.DisplayLayout
-import com.android.wm.shell.common.MultiInstanceHelper
-import com.android.wm.shell.common.SyncTransactionQueue
-import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
-import com.android.wm.shell.desktopmode.DesktopModeEventLogger
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
-import com.android.wm.shell.desktopmode.DesktopRepository
-import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
-import com.android.wm.shell.desktopmode.DesktopTasksLimiter
-import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
-import com.android.wm.shell.desktopmode.education.AppHandleEducationController
-import com.android.wm.shell.desktopmode.education.AppToWebEducationController
-import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.splitscreen.SplitScreenController
-import com.android.wm.shell.sysui.ShellCommandHandler
-import com.android.wm.shell.sysui.ShellController
-import com.android.wm.shell.sysui.ShellInit
-import com.android.wm.shell.transition.FocusTransitionObserver
-import com.android.wm.shell.transition.Transitions
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
-import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
-import java.util.Optional
-import java.util.function.Consumer
-import java.util.function.Supplier
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentCaptor.forClass
-import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.kotlin.KArgumentCaptor
-import org.mockito.kotlin.verify
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.argThat
@@ -137,9 +86,10 @@
 import org.mockito.kotlin.doNothing
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
+import java.util.function.Consumer
 
 
 /**
@@ -150,72 +100,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
-class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
-    @JvmField
-    @Rule
-    val setFlagsRule = SetFlagsRule()
-
-    @JvmField
-    @Rule
-    val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
-
-    @Mock private lateinit var mockDesktopModeWindowDecorFactory:
-            DesktopModeWindowDecoration.Factory
-    @Mock private lateinit var mockMainHandler: Handler
-    @Mock private lateinit var mockMainChoreographer: Choreographer
-    @Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
-    @Mock private lateinit var mockDisplayController: DisplayController
-    @Mock private lateinit var mockSplitScreenController: SplitScreenController
-    @Mock private lateinit var mockDesktopRepository: DesktopRepository
-    @Mock private lateinit var mockDisplayLayout: DisplayLayout
-    @Mock private lateinit var displayInsetsController: DisplayInsetsController
-    @Mock private lateinit var mockSyncQueue: SyncTransactionQueue
-    @Mock private lateinit var mockDesktopTasksController: DesktopTasksController
-    @Mock private lateinit var mockInputMonitor: InputMonitor
-    @Mock private lateinit var mockTransitions: Transitions
-    @Mock private lateinit var mockInputMonitorFactory:
-            DesktopModeWindowDecorViewModel.InputMonitorFactory
-    @Mock private lateinit var mockShellController: ShellController
-    private val testShellExecutor = TestShellExecutor()
-    @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
-    @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
-    @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
-    @Mock private lateinit var mockWindowManager: IWindowManager
-    @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
-    @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser
-    @Mock private lateinit var mockUserHandle: UserHandle
-    @Mock private lateinit var mockAssistContentRequester: AssistContentRequester
-    private val bgExecutor = TestShellExecutor()
-    @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper
-    @Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter
-    @Mock private lateinit var mockFreeformTaskTransitionStarter: FreeformTaskTransitionStarter
-    @Mock private lateinit var mockActivityOrientationChangeHandler:
-            DesktopActivityOrientationChangeHandler
-    @Mock private lateinit var mockInputManager: InputManager
-    @Mock private lateinit var mockTaskPositionerFactory:
-            DesktopModeWindowDecorViewModel.TaskPositionerFactory
-    @Mock private lateinit var mockTaskPositioner: TaskPositioner
-    @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
-    @Mock private lateinit var mockAppToWebEducationController: AppToWebEducationController
-    @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
-    @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
-    @Mock private lateinit var motionEvent: MotionEvent
-    @Mock lateinit var displayController: DisplayController
-    @Mock lateinit var displayLayout: DisplayLayout
-    private lateinit var spyContext: TestableContext
-    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
-
-    private val transactionFactory = Supplier<SurfaceControl.Transaction> {
-        SurfaceControl.Transaction()
-    }
-    private val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>())
-
-    private lateinit var mockitoSession: StaticMockitoSession
-    private lateinit var shellInit: ShellInit
-    private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
-    private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener
-    private lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
-    private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
+class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTestsBase() {
 
     @Before
     fun setUp() {
@@ -225,98 +110,11 @@
                 .spyStatic(DesktopModeStatus::class.java)
                 .spyStatic(DragPositioningCallbackUtility::class.java)
                 .startMocking()
+
         doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) }
+        doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
 
-        spyContext = spy(mContext)
-        doNothing().`when`(spyContext).startActivity(any())
-        shellInit = ShellInit(testShellExecutor)
-        windowDecorByTaskIdSpy.clear()
-        spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
-        desktopModeEventLogger = mock<DesktopModeEventLogger>()
-        desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
-                spyContext,
-                testShellExecutor,
-                mockMainHandler,
-                mockMainChoreographer,
-                bgExecutor,
-                shellInit,
-                mockShellCommandHandler,
-                mockWindowManager,
-                mockTaskOrganizer,
-                mockDesktopRepository,
-                mockDisplayController,
-                mockShellController,
-                displayInsetsController,
-                mockSyncQueue,
-                mockTransitions,
-                Optional.of(mockDesktopTasksController),
-                mockGenericLinksParser,
-                mockAssistContentRequester,
-                mockMultiInstanceHelper,
-                mockDesktopModeWindowDecorFactory,
-                mockInputMonitorFactory,
-                transactionFactory,
-                mockAppHeaderViewHolderFactory,
-                mockRootTaskDisplayAreaOrganizer,
-                windowDecorByTaskIdSpy,
-                mockInteractionJankMonitor,
-                Optional.of(mockTasksLimiter),
-                mockAppHandleEducationController,
-                mockAppToWebEducationController,
-                mockCaptionHandleRepository,
-                Optional.of(mockActivityOrientationChangeHandler),
-                mockTaskPositionerFactory,
-                mockFocusTransitionObserver,
-                desktopModeEventLogger
-        )
-        desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
-        whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
-        whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
-        whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor)
-        whenever(
-            mockTaskPositionerFactory.create(
-                any(),
-                any(),
-                any(),
-                any(),
-                any(),
-                any(),
-                any(),
-                any()
-            )
-        )
-            .thenReturn(mockTaskPositioner)
-
-        // InputChannel cannot be mocked because it passes to InputEventReceiver.
-        val inputChannels = InputChannel.openInputChannelPair(TAG)
-        inputChannels.first().dispose()
-        whenever(mockInputMonitor.inputChannel).thenReturn(inputChannels[1])
-
-        shellInit.init()
-
-        val displayChangingListenerCaptor =
-            argumentCaptor<DisplayChangeController.OnDisplayChangingListener>()
-        verify(mockDisplayController)
-            .addDisplayChangingController(displayChangingListenerCaptor.capture())
-        displayChangingListener = displayChangingListenerCaptor.firstValue
-        val insetsChangedCaptor =
-                argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
-        verify(displayInsetsController)
-            .addGlobalInsetsChangedListener(insetsChangedCaptor.capture())
-        desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue
-        val keyguardChangedCaptor =
-            argumentCaptor<DesktopModeKeyguardChangeListener>()
-        verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
-        desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue
-        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
-        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
-            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
-        }
-    }
-
-    @After
-    fun tearDown() {
-        mockitoSession.finishMocking()
+        setUpCommon()
     }
 
     @Test
@@ -559,20 +357,6 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
-    fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() {
-        // Simulate default enforce device restrictions system property
-        whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
-
-        val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
-        // Simulate device that doesn't support desktop mode
-        doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-
-        onTaskOpening(task)
-        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() {
         // Simulate enforce device restrictions system property overridden to false
         whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
@@ -1419,64 +1203,6 @@
         return decor
     }
 
-    private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
-        desktopModeWindowDecorViewModel.onTaskOpening(
-                task,
-                leash,
-                SurfaceControl.Transaction(),
-                SurfaceControl.Transaction()
-        )
-    }
-
-    private fun onTaskChanging(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
-        desktopModeWindowDecorViewModel.onTaskChanging(
-                task,
-                leash,
-                SurfaceControl.Transaction(),
-                SurfaceControl.Transaction()
-        )
-    }
-
-    private fun createTask(
-            displayId: Int = DEFAULT_DISPLAY,
-            @WindowingMode windowingMode: Int,
-            activityType: Int = ACTIVITY_TYPE_STANDARD,
-            activityInfo: ActivityInfo = ActivityInfo(),
-            requestingImmersive: Boolean = false
-    ): RunningTaskInfo {
-        return TestRunningTaskInfoBuilder()
-                .setDisplayId(displayId)
-                .setWindowingMode(windowingMode)
-                .setVisible(true)
-                .setActivityType(activityType)
-                .build().apply {
-                    topActivityInfo = activityInfo
-                    isResizeable = true
-                    requestedVisibleTypes = if (requestingImmersive) {
-                        statusBars().inv()
-                    } else {
-                        statusBars()
-                    }
-                }
-    }
-
-    private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
-        val decoration = mock(DesktopModeWindowDecoration::class.java)
-        whenever(
-            mockDesktopModeWindowDecorFactory.create(
-                any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
-                any(), any(), any(), any(), any(), any(), any(), any())
-        ).thenReturn(decoration)
-        decoration.mTaskInfo = task
-        whenever(decoration.user).thenReturn(mockUserHandle)
-        if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
-            whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
-                .thenReturn(true)
-        }
-        whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600))
-        return decoration
-    }
-
     private fun setUpMockDecorationsForTasks(vararg tasks: RunningTaskInfo) {
         tasks.forEach { setUpMockDecorationForTask(it) }
     }
@@ -1493,19 +1219,4 @@
                 DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
         )
     }
-
-    private fun RunningTaskInfo.setWindowingMode(@WindowingMode mode: Int) {
-        configuration.windowConfiguration.windowingMode = mode
-    }
-
-    private fun RunningTaskInfo.setActivityType(type: Int) {
-        configuration.windowConfiguration.activityType = type
-    }
-
-    companion object {
-        private const val TAG = "DesktopModeWindowDecorViewModelTests"
-        private val STABLE_INSETS = Rect(0, 100, 0, 0)
-        private val INITIAL_BOUNDS = Rect(0, 0, 100, 100)
-        private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
new file mode 100644
index 0000000..91eaada
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -0,0 +1,327 @@
+/*
+ * 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
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WindowingMode
+import android.content.pm.ActivityInfo
+import android.graphics.Rect
+import android.hardware.input.InputManager
+import android.os.Handler
+import android.os.UserHandle
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.TestableContext
+import android.util.SparseArray
+import android.view.Choreographer
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import android.view.InputChannel
+import android.view.InputMonitor
+import android.view.MotionEvent
+import android.view.SurfaceControl
+import android.view.WindowInsets.Type.statusBars
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
+import com.android.wm.shell.apptoweb.AssistContentRequester
+import com.android.wm.shell.common.DisplayChangeController
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayInsetsController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.MultiInstanceHelper
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTasksLimiter
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
+import com.android.wm.shell.desktopmode.education.AppHandleEducationController
+import com.android.wm.shell.desktopmode.education.AppToWebEducationController
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.splitscreen.SplitScreenController
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellController
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.FocusTransitionObserver
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.StubTransaction
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
+import org.junit.After
+import org.junit.Rule
+import org.mockito.Mockito
+import org.mockito.Mockito.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import java.util.Optional
+import java.util.function.Supplier
+
+/**
+ * Utility class for tests of [DesktopModeWindowDecorViewModel]
+ */
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
+    @JvmField
+    @Rule
+    val setFlagsRule = SetFlagsRule()
+
+    @JvmField
+    @Rule
+    val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    private val mockDesktopModeWindowDecorFactory = mock<DesktopModeWindowDecoration.Factory>()
+    protected val mockMainHandler = mock<Handler>()
+    protected val mockMainChoreographer = mock<Choreographer>()
+    protected val mockTaskOrganizer = mock<ShellTaskOrganizer>()
+    protected val mockDisplayController = mock<DisplayController>()
+    protected val mockSplitScreenController = mock<SplitScreenController>()
+    protected val mockDesktopRepository = mock<DesktopRepository>()
+    protected val mockDisplayLayout = mock<DisplayLayout>()
+    protected val displayInsetsController = mock<DisplayInsetsController>()
+    protected val mockSyncQueue = mock<SyncTransactionQueue>()
+    protected val mockDesktopTasksController = mock<DesktopTasksController>()
+    protected val mockInputMonitor = mock<InputMonitor>()
+    protected val mockTransitions = mock<Transitions>()
+    internal val mockInputMonitorFactory =
+        mock<DesktopModeWindowDecorViewModel.InputMonitorFactory>()
+    protected val mockShellController = mock<ShellController>()
+    protected val testShellExecutor = TestShellExecutor()
+    protected val mockAppHeaderViewHolderFactory = mock<AppHeaderViewHolder.Factory>()
+    protected val mockRootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+    protected val mockShellCommandHandler = mock<ShellCommandHandler>()
+    protected val mockWindowManager = mock<IWindowManager>()
+    protected val mockInteractionJankMonitor = mock<InteractionJankMonitor>()
+    protected val mockGenericLinksParser = mock<AppToWebGenericLinksParser>()
+    protected val mockUserHandle = mock<UserHandle>()
+    protected val mockAssistContentRequester = mock<AssistContentRequester>()
+    protected val bgExecutor = TestShellExecutor()
+    protected val mockMultiInstanceHelper = mock<MultiInstanceHelper>()
+    protected val mockTasksLimiter = mock<DesktopTasksLimiter>()
+    protected val mockFreeformTaskTransitionStarter = mock<FreeformTaskTransitionStarter>()
+    protected val mockActivityOrientationChangeHandler =
+        mock<DesktopActivityOrientationChangeHandler>()
+    protected val mockInputManager = mock<InputManager>()
+    private val mockTaskPositionerFactory =
+        mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>()
+    protected val mockTaskPositioner = mock<TaskPositioner>()
+    protected val mockAppHandleEducationController = mock<AppHandleEducationController>()
+    protected val mockAppToWebEducationController = mock<AppToWebEducationController>()
+    protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>()
+    protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
+    protected val motionEvent = mock<MotionEvent>()
+    val displayController = mock<DisplayController>()
+    val displayLayout = mock<DisplayLayout>()
+    protected lateinit var spyContext: TestableContext
+    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+
+    private val transactionFactory = Supplier<SurfaceControl.Transaction> {
+        SurfaceControl.Transaction()
+    }
+    protected val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>())
+
+    protected lateinit var mockitoSession: StaticMockitoSession
+    protected lateinit var shellInit: ShellInit
+    internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+    protected lateinit var displayChangingListener:
+            DisplayChangeController.OnDisplayChangingListener
+    internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
+    protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
+
+    fun setUpCommon() {
+        spyContext = spy(mContext)
+        doNothing().`when`(spyContext).startActivity(any())
+        shellInit = ShellInit(testShellExecutor)
+        windowDecorByTaskIdSpy.clear()
+        spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
+        desktopModeEventLogger = mock<DesktopModeEventLogger>()
+        desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
+            spyContext,
+            testShellExecutor,
+            mockMainHandler,
+            mockMainChoreographer,
+            bgExecutor,
+            shellInit,
+            mockShellCommandHandler,
+            mockWindowManager,
+            mockTaskOrganizer,
+            mockDesktopRepository,
+            mockDisplayController,
+            mockShellController,
+            displayInsetsController,
+            mockSyncQueue,
+            mockTransitions,
+            Optional.of(mockDesktopTasksController),
+            mockGenericLinksParser,
+            mockAssistContentRequester,
+            mockMultiInstanceHelper,
+            mockDesktopModeWindowDecorFactory,
+            mockInputMonitorFactory,
+            transactionFactory,
+            mockAppHeaderViewHolderFactory,
+            mockRootTaskDisplayAreaOrganizer,
+            windowDecorByTaskIdSpy,
+            mockInteractionJankMonitor,
+            Optional.of(mockTasksLimiter),
+            mockAppHandleEducationController,
+            mockAppToWebEducationController,
+            mockCaptionHandleRepository,
+            Optional.of(mockActivityOrientationChangeHandler),
+            mockTaskPositionerFactory,
+            mockFocusTransitionObserver,
+            desktopModeEventLogger
+        )
+        desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
+        whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
+        whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor)
+        whenever(
+            mockTaskPositionerFactory.create(
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any()
+            )
+        )
+            .thenReturn(mockTaskPositioner)
+
+        // InputChannel cannot be mocked because it passes to InputEventReceiver.
+        val inputChannels = InputChannel.openInputChannelPair(TAG)
+        inputChannels.first().dispose()
+        whenever(mockInputMonitor.inputChannel).thenReturn(inputChannels[1])
+
+        shellInit.init()
+
+        val displayChangingListenerCaptor =
+            argumentCaptor<DisplayChangeController.OnDisplayChangingListener>()
+        verify(mockDisplayController)
+            .addDisplayChangingController(displayChangingListenerCaptor.capture())
+        displayChangingListener = displayChangingListenerCaptor.firstValue
+        val insetsChangedCaptor =
+            argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
+        verify(displayInsetsController)
+            .addGlobalInsetsChangedListener(insetsChangedCaptor.capture())
+        desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue
+        val keyguardChangedCaptor =
+            argumentCaptor<DesktopModeKeyguardChangeListener>()
+        verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
+        desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    protected fun createTask(
+        displayId: Int = DEFAULT_DISPLAY,
+        @WindowingMode windowingMode: Int,
+        activityType: Int = ACTIVITY_TYPE_STANDARD,
+        activityInfo: ActivityInfo = ActivityInfo(),
+        requestingImmersive: Boolean = false
+    ): RunningTaskInfo {
+        return TestRunningTaskInfoBuilder()
+            .setDisplayId(displayId)
+            .setWindowingMode(windowingMode)
+            .setVisible(true)
+            .setActivityType(activityType)
+            .build().apply {
+                topActivityInfo = activityInfo
+                isResizeable = true
+                requestedVisibleTypes = if (requestingImmersive) {
+                    statusBars().inv()
+                } else {
+                    statusBars()
+                }
+            }
+    }
+
+    protected fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
+        val decoration = Mockito.mock(DesktopModeWindowDecoration::class.java)
+        whenever(
+            mockDesktopModeWindowDecorFactory.create(
+                any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+                any(), any(), any(), any(), any(), any(), any(), any())
+        ).thenReturn(decoration)
+        decoration.mTaskInfo = task
+        whenever(decoration.user).thenReturn(mockUserHandle)
+        if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+            whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
+                .thenReturn(true)
+        }
+        whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600))
+        return decoration
+    }
+
+
+    protected fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
+        desktopModeWindowDecorViewModel.onTaskOpening(
+            task,
+            leash,
+            StubTransaction(),
+            StubTransaction()
+        )
+    }
+
+    protected fun onTaskChanging(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
+        desktopModeWindowDecorViewModel.onTaskChanging(
+            task,
+            leash,
+            StubTransaction(),
+            StubTransaction()
+        )
+    }
+
+    protected fun RunningTaskInfo.setWindowingMode(@WindowingMode mode: Int) {
+        configuration.windowConfiguration.windowingMode = mode
+    }
+
+    protected fun RunningTaskInfo.setActivityType(type: Int) {
+        configuration.windowConfiguration.activityType = type
+    }
+
+    companion object {
+        const val TAG = "DesktopModeWindowDecorViewModelTestsBase"
+        val STABLE_INSETS = Rect(0, 100, 0, 0)
+        val INITIAL_BOUNDS = Rect(0, 0, 100, 100)
+        val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+    }
+}
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index fb58a69..b72e066 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -84,6 +84,7 @@
     // this value is only valid after the GPU has been initialized and there is a valid graphics
     // context or if you are using the HWUI_NULL_GPU
     int maxTextureSize() const;
+    bool hasMaxTextureSize() const { return mMaxTextureSize > 0; }
     sk_sp<SkColorSpace> getWideColorSpace() const { return mWideColorSpace; }
     SkColorType getWideColorType() {
         static std::once_flag kFlag;
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 2c23864..4801bd1 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -186,7 +186,7 @@
     // If we are not a layer OR we cannot be rendered (eg, view was detached)
     // we need to destroy any Layers we may have had previously
     if (CC_LIKELY(layerType != LayerType::RenderLayer) || CC_UNLIKELY(!isRenderable()) ||
-        CC_UNLIKELY(properties().getWidth() == 0) || CC_UNLIKELY(properties().getHeight() == 0) ||
+        CC_UNLIKELY(properties().getWidth() <= 0) || CC_UNLIKELY(properties().getHeight() <= 0) ||
         CC_UNLIKELY(!properties().fitsOnLayer())) {
         if (CC_UNLIKELY(hasLayer())) {
             this->setLayerSurface(nullptr);
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index b1ad8b2..4dc5700 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -545,7 +545,8 @@
     bool fitsOnLayer() const {
         const DeviceInfo* deviceInfo = DeviceInfo::get();
         return mPrimitiveFields.mWidth <= deviceInfo->maxTextureSize() &&
-               mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize();
+               mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize() &&
+               mPrimitiveFields.mWidth > 0 && mPrimitiveFields.mHeight > 0;
     }
 
     bool promotedToLayer() const {
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 8ec0430..b36b8be 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -418,6 +418,11 @@
                                 RenderNode* target) {
     mRenderThread.removeFrameCallback(this);
 
+    // Make sure we have a valid device info
+    if (!DeviceInfo::get()->hasMaxTextureSize()) {
+        (void)mRenderThread.requireGrContext();
+    }
+
     // If the previous frame was dropped we don't need to hold onto it, so
     // just keep using the previous frame's structure instead
     const auto reason = wasSkipped(mCurrentFrameInfo);
diff --git a/libs/hwui/tests/unit/RenderPropertiesTests.cpp b/libs/hwui/tests/unit/RenderPropertiesTests.cpp
index 3e8e057..6ec042c 100644
--- a/libs/hwui/tests/unit/RenderPropertiesTests.cpp
+++ b/libs/hwui/tests/unit/RenderPropertiesTests.cpp
@@ -40,7 +40,11 @@
     props.setLeftTopRightBottom(0, 0, maxTextureSize + 1, maxTextureSize + 1);
     ASSERT_FALSE(props.fitsOnLayer());
 
-    // Too small, but still 'fits'. Not fitting is an error case, so don't report empty as such.
+    // Too small, we can't create a layer for a 0 width or height
     props.setLeftTopRightBottom(0, 0, 100, 0);
-    ASSERT_TRUE(props.fitsOnLayer());
+    ASSERT_FALSE(props.fitsOnLayer());
+
+    // Can't create a negative-sized layer
+    props.setLeftTopRightBottom(0, 0, -100, 300);
+    ASSERT_FALSE(props.fitsOnLayer());
 }
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 4330f0a..d993b87 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -28,15 +28,13 @@
 #define INDENT "  "
 #define INDENT2 "    "
 
-namespace android {
-
 namespace {
-
 // Time to spend fading out the pointer completely.
 const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms
-
 } // namespace
 
+namespace android {
+
 // --- MouseCursorController ---
 
 MouseCursorController::MouseCursorController(PointerControllerContext& context)
@@ -66,21 +64,17 @@
     mLocked.pointerSprite.clear();
 }
 
-vec2 MouseCursorController::move(float deltaX, float deltaY) {
+void MouseCursorController::move(float deltaX, float deltaY) {
 #if DEBUG_MOUSE_CURSOR_UPDATES
     ALOGD("Move pointer by deltaX=%0.3f, deltaY=%0.3f", deltaX, deltaY);
 #endif
     if (deltaX == 0.0f && deltaY == 0.0f) {
-        return {};
+        return;
     }
 
-    // When transition occurs, the MouseCursorController object may or may not be deleted, depending
-    // if there's another display on the other side of the transition. At this point we still need
-    // to move the cursor to the boundary.
     std::scoped_lock lock(mLock);
-    const vec2 pos{mLocked.pointerX + deltaX, mLocked.pointerY + deltaY};
-    setPositionLocked(pos.x, pos.y);
-    return vec2{pos.x - mLocked.pointerX, pos.y - mLocked.pointerY};
+
+    setPositionLocked(mLocked.pointerX + deltaX, mLocked.pointerY + deltaY);
 }
 
 void MouseCursorController::setPosition(float x, float y) {
@@ -88,26 +82,22 @@
     ALOGD("Set pointer position to x=%0.3f, y=%0.3f", x, y);
 #endif
     std::scoped_lock lock(mLock);
-
     setPositionLocked(x, y);
 }
 
-FloatRect MouseCursorController::getBoundsLocked() REQUIRES(mLock) {
+void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
+    const auto& v = mLocked.viewport;
+    if (!v.isValid()) return;
+
     // The valid bounds for a mouse cursor. Since the right and bottom edges are considered outside
     // the display, clip the bounds by one pixel instead of letting the cursor get arbitrarily
     // close to the outside edge.
-    return FloatRect{
+    const FloatRect bounds{
             static_cast<float>(mLocked.viewport.logicalLeft),
             static_cast<float>(mLocked.viewport.logicalTop),
             static_cast<float>(mLocked.viewport.logicalRight - 1),
             static_cast<float>(mLocked.viewport.logicalBottom - 1),
     };
-}
-void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
-    const auto& v = mLocked.viewport;
-    if (!v.isValid()) return;
-
-    const FloatRect bounds = getBoundsLocked();
     mLocked.pointerX = std::max(bounds.left, std::min(bounds.right, x));
     mLocked.pointerY = std::max(bounds.top, std::min(bounds.bottom, y));
 
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 10f0d76..12b31a8 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -20,6 +20,9 @@
 #include <gui/DisplayEventReceiver.h>
 #include <input/DisplayViewport.h>
 #include <input/Input.h>
+#include <utils/BitSet.h>
+#include <utils/Looper.h>
+#include <utils/RefBase.h>
 
 #include <functional>
 #include <map>
@@ -40,8 +43,7 @@
     MouseCursorController(PointerControllerContext& context);
     ~MouseCursorController();
 
-    // Move the pointer and return unconsumed delta
-    vec2 move(float deltaX, float deltaY);
+    void move(float deltaX, float deltaY);
     void setPosition(float x, float y);
     FloatPoint getPosition() const;
     ui::LogicalDisplayId getDisplayId() const;
@@ -111,7 +113,6 @@
     bool doFadingAnimationLocked(nsecs_t timestamp);
 
     void startAnimationLocked();
-    FloatRect getBoundsLocked();
 };
 
 } // namespace android
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 59397da..78d7d3a 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -138,18 +138,15 @@
     return mDisplayInfoListener->mLock;
 }
 
-vec2 PointerController::move(float deltaX, float deltaY) {
+void PointerController::move(float deltaX, float deltaY) {
     const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
-    ui::Transform transform;
+    vec2 transformed;
     {
         std::scoped_lock lock(getLock());
-        transform = getTransformForDisplayLocked(displayId);
+        const auto& transform = getTransformForDisplayLocked(displayId);
+        transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
     }
-
-    vec2 transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
-
-    vec2 unconsumedDelta = mCursorController.move(transformed.x, transformed.y);
-    return transformWithoutTranslation(transform.inverse(), unconsumedDelta);
+    mCursorController.move(transformed.x, transformed.y);
 }
 
 void PointerController::setPosition(float x, float y) {
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 69bef54..ee8d121 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -51,7 +51,7 @@
 
     ~PointerController() override;
 
-    vec2 move(float deltaX, float deltaY) override;
+    void move(float deltaX, float deltaY) override;
     void setPosition(float x, float y) override;
     FloatPoint getPosition() const override;
     ui::LogicalDisplayId getDisplayId() const override;
@@ -165,7 +165,7 @@
 
     ~TouchPointerController() override;
 
-    vec2 move(float, float) override {
+    void move(float, float) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
     void setPosition(float, float) override {
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index 80c934a..5b00fca 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -40,8 +40,6 @@
     CURSOR_TYPE_CUSTOM = -1,
 };
 
-static constexpr float EPSILON = MotionEvent::ROUNDING_PRECISION;
-
 using ::testing::AllOf;
 using ::testing::Field;
 using ::testing::NiceMock;
@@ -401,135 +399,6 @@
                          testing::Values(PointerControllerInterface::ControllerType::MOUSE,
                                          PointerControllerInterface::ControllerType::STYLUS));
 
-class MousePointerControllerTest : public PointerControllerTest {
-protected:
-    MousePointerControllerTest() {
-        sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>);
-        EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite));
-
-        // create a mouse pointer controller
-        mPointerController =
-                PointerController::create(mPolicy, mLooper, *mSpriteController,
-                                          PointerControllerInterface::ControllerType::MOUSE);
-
-        // set display viewport
-        DisplayViewport viewport;
-        viewport.displayId = ui::LogicalDisplayId::DEFAULT;
-        viewport.logicalRight = 5;
-        viewport.logicalBottom = 5;
-        viewport.physicalRight = 5;
-        viewport.physicalBottom = 5;
-        viewport.deviceWidth = 5;
-        viewport.deviceHeight = 5;
-        mPointerController->setDisplayViewport(viewport);
-    }
-};
-
-struct MousePointerControllerTestParam {
-    vec2 startPosition;
-    vec2 delta;
-    vec2 endPosition;
-    vec2 unconsumedDelta;
-};
-
-class PointerControllerViewportTransitionTest
-      : public MousePointerControllerTest,
-        public testing::WithParamInterface<MousePointerControllerTestParam> {};
-
-TEST_P(PointerControllerViewportTransitionTest, testPointerViewportTransition) {
-    const auto& params = GetParam();
-
-    mPointerController->setPosition(params.startPosition.x, params.startPosition.y);
-    auto unconsumedDelta = mPointerController->move(params.delta.x, params.delta.y);
-
-    auto position = mPointerController->getPosition();
-    EXPECT_NEAR(position.x, params.endPosition.x, EPSILON);
-    EXPECT_NEAR(position.y, params.endPosition.y, EPSILON);
-    EXPECT_NEAR(unconsumedDelta.x, params.unconsumedDelta.x, EPSILON);
-    EXPECT_NEAR(unconsumedDelta.y, params.unconsumedDelta.y, EPSILON);
-}
-
-INSTANTIATE_TEST_SUITE_P(PointerControllerViewportTransitionTest,
-                         PointerControllerViewportTransitionTest,
-                         testing::Values(
-                                 // no transition
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {2.0f, 2.0f},
-                                                                 {4.0f, 4.0f},
-                                                                 {0.0f, 0.0f}},
-                                 // right boundary
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {3.0f, 0.0f},
-                                                                 {4.0f, 2.0f},
-                                                                 {1.0f, 0.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {3.0f, -1.0f},
-                                                                 {4.0f, 1.0f},
-                                                                 {1.0f, 0.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {3.0f, 1.0f},
-                                                                 {4.0f, 3.0f},
-                                                                 {1.0f, 0.0f}},
-                                 // left boundary
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-3.0f, 0.0f},
-                                                                 {0.0f, 2.0f},
-                                                                 {-1.0f, 0.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-3.0f, -1.0f},
-                                                                 {0.0f, 1.0f},
-                                                                 {-1.0f, 0.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-3.0f, 1.0f},
-                                                                 {0.0f, 3.0f},
-                                                                 {-1.0f, 0.0f}},
-                                 // bottom boundary
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {0.0f, 3.0f},
-                                                                 {2.0f, 4.0f},
-                                                                 {0.0f, 1.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-1.0f, 3.0f},
-                                                                 {1.0f, 4.0f},
-                                                                 {0.0f, 1.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {1.0f, 3.0f},
-                                                                 {3.0f, 4.0f},
-                                                                 {0.0f, 1.0f}},
-                                 // top boundary
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {0.0f, -3.0f},
-                                                                 {2.0f, 0.0f},
-                                                                 {0.0f, -1.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-1.0f, -3.0f},
-                                                                 {1.0f, 0.0f},
-                                                                 {0.0f, -1.0f}},
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {1.0f, -3.0f},
-                                                                 {3.0f, 0.0f},
-                                                                 {0.0f, -1.0f}},
-                                 // top-left corner
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-3.0f, -3.0f},
-                                                                 {0.0f, 0.0f},
-                                                                 {-1.0f, -1.0f}},
-                                 // top-right corner
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {3.0f, -3.0f},
-                                                                 {4.0f, 0.0f},
-                                                                 {1.0f, -1.0f}},
-                                 // bottom-right corner
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {3.0f, 3.0f},
-                                                                 {4.0f, 4.0f},
-                                                                 {1.0f, 1.0f}},
-                                 // bottom-left corner
-                                 MousePointerControllerTestParam{{2.0f, 2.0f},
-                                                                 {-3.0f, 3.0f},
-                                                                 {0.0f, 4.0f},
-                                                                 {-1.0f, 1.0f}}));
-
 class PointerControllerWindowInfoListenerTest : public Test {};
 
 TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index f5913c7..4b3962e 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -22,6 +22,7 @@
 
 import com.android.aconfig.annotations.VisibleForTesting;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -68,14 +69,14 @@
         return new AudioDevicePort(
                 new AudioHandle(/* id= */ 0),
                 /* name= */ "testAudioDevicePort",
-                /* profiles= */ null,
+                /* profiles= */ new ArrayList<>(),
                 /* gains= */ null,
                 /* type= */ AudioManager.DEVICE_OUT_SPEAKER,
                 /* address= */ "testAddress",
                 /* speakerLayoutChannelMask= */ speakerLayoutChannelMask,
                 /* encapsulationModes= */ null,
                 /* encapsulationMetadataTypes= */ null,
-                /* descriptors= */ null);
+                /* descriptors= */ new ArrayList<>());
     }
 
     private final int mType;
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 1bb9a8e..b4dee0c 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -4,14 +4,6 @@
 # Project link: https://gantry.corp.google.com/projects/android_platform_window_surfaces/changes
 
 flag {
-    name: "limit_manage_media_projection"
-    namespace: "lse_desktop_experience"
-    description: "Limit signature permission manage_media_projection to the SystemUI role"
-    bug: "323008518"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "media_projection_connected_display"
     namespace: "virtual_devices"
     description: "Enable recording connected display"
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl
index aaedf21..1c85c7b 100644
--- a/media/java/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/IMediaQualityManager.aidl
@@ -21,6 +21,7 @@
 import android.media.quality.IPictureProfileCallback;
 import android.media.quality.ISoundProfileCallback;
 import android.media.quality.ParamCapability;
+import android.media.quality.PictureProfileHandle;
 import android.media.quality.PictureProfile;
 import android.media.quality.SoundProfile;
 
@@ -38,6 +39,7 @@
     List<String> getPictureProfilePackageNames();
     List<String> getPictureProfileAllowList();
     void setPictureProfileAllowList(in List<String> packages);
+    PictureProfileHandle getPictureProfileHandle(in String id);
 
     SoundProfile createSoundProfile(in SoundProfile pp);
     void updateSoundProfile(in String id, in SoundProfile pp);
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index 28fe9b6..43e884a 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -271,6 +271,17 @@
         }
     }
 
+    /**
+     * Gets picture profile handle by profile ID.
+     * @hide
+     */
+    public PictureProfileHandle getPictureProfileHandle(String id) {
+        try {
+            return mService.getPictureProfileHandle(id);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * Creates a picture profile and store it in the system.
diff --git a/media/java/android/media/quality/PictureProfileHandle.java b/media/java/android/media/quality/PictureProfileHandle.java
index 2b1cda4..714fd36 100644
--- a/media/java/android/media/quality/PictureProfileHandle.java
+++ b/media/java/android/media/quality/PictureProfileHandle.java
@@ -17,46 +17,58 @@
 package android.media.quality;
 
 import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import androidx.annotation.NonNull;
-
-// TODO(b/337330263): Expose as public API after API review
 /**
-  * A type-safe handle to a picture profile, which represents a collection of parameters used to
-  * configure picture processing hardware to enhance the quality of graphic buffers.
+  * A type-safe handle to a picture profile used to apply picture processing to a SurfaceControl.
+  *
+  * A picture profile represents a collection of parameters used to configure picture processing
+  * to enhance the quality of graphic buffers.
+  *
   * @hide
   */
-@FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+@SystemApi
+@FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
 public final class PictureProfileHandle implements Parcelable {
+    public static final @NonNull PictureProfileHandle NONE = new PictureProfileHandle(0);
+
     private final long mId;
 
-    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    /** @hide */
     public PictureProfileHandle(long id) {
         mId = id;
     }
 
-    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    /** @hide */
+    @SystemApi
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
     public long getId() {
         return mId;
     }
 
-    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    /** @hide */
+    @SystemApi
     @Override
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeLong(mId);
     }
 
-    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    /** @hide */
+    @SystemApi
     @Override
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
     public int describeContents() {
         return 0;
     }
 
-    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
-    @NonNull
-    public static final Creator<PictureProfileHandle> CREATOR =
+    /** @hide */
+    @SystemApi
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
+    public static final @NonNull Creator<PictureProfileHandle> CREATOR =
             new Creator<PictureProfileHandle>() {
                 @Override
                 public PictureProfileHandle createFromParcel(Parcel in) {
diff --git a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
index 898a8bf..4de7123 100644
--- a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
+++ b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
@@ -63,7 +63,7 @@
             FRONTEND_STATUS_TYPE_DVBT_CELL_IDS, FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO,
             FRONTEND_STATUS_TYPE_IPTV_CONTENT_URL, FRONTEND_STATUS_TYPE_IPTV_PACKETS_LOST,
             FRONTEND_STATUS_TYPE_IPTV_PACKETS_RECEIVED, FRONTEND_STATUS_TYPE_IPTV_WORST_JITTER_MS,
-            FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXT})
+            FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXTENSION})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FrontendStatusType {}
 
@@ -317,7 +317,7 @@
      * Standard extension.
      */
     @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
-    public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT =
+    public static final int FRONTEND_STATUS_TYPE_STANDARD_EXTENSION =
             android.hardware.tv.tuner.FrontendStatusType.STANDARD_EXT;
 
     /** @hide */
@@ -567,7 +567,7 @@
     private Long mIptvPacketsReceived;
     private Integer mIptvWorstJitterMs;
     private Integer mIptvAverageJitterMs;
-    private StandardExt mStandardExt;
+    private StandardExtension mStandardExtension;
 
     // Constructed and fields set by JNI code.
     private FrontendStatus() {
@@ -1298,12 +1298,12 @@
      */
     @NonNull
     @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
-    public StandardExt getStandardExt() {
+    public StandardExtension getStandardExtension() {
         TunerVersionChecker.checkHigherOrEqualVersionTo(
-                TunerVersionChecker.TUNER_VERSION_4_0, "StandardExt status");
-        if (mStandardExt == null) {
-            throw new IllegalStateException("StandardExt status is empty");
+                TunerVersionChecker.TUNER_VERSION_4_0, "StandardExtension status");
+        if (mStandardExtension == null) {
+            throw new IllegalStateException("StandardExtension status is empty");
         }
-        return mStandardExt;
+        return mStandardExtension;
     }
 }
diff --git a/media/java/android/media/tv/tuner/frontend/StandardExt.java b/media/java/android/media/tv/tuner/frontend/StandardExtension.java
similarity index 74%
rename from media/java/android/media/tv/tuner/frontend/StandardExt.java
rename to media/java/android/media/tv/tuner/frontend/StandardExtension.java
index 4907272..8bff3dd 100644
--- a/media/java/android/media/tv/tuner/frontend/StandardExt.java
+++ b/media/java/android/media/tv/tuner/frontend/StandardExtension.java
@@ -29,16 +29,16 @@
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
-public final class StandardExt {
-    private final int mDvbsStandardExt;
-    private final int mDvbtStandardExt;
+public final class StandardExtension {
+    private final int mDvbsStandardExtension;
+    private final int mDvbtStandardExtension;
 
     /**
      * Private constructor called by JNI only.
      */
-    private StandardExt(int dvbsStandardExt, int dvbtStandardExt) {
-        mDvbsStandardExt = dvbsStandardExt;
-        mDvbtStandardExt = dvbtStandardExt;
+    private StandardExtension(int dvbsStandardExtension, int dvbtStandardExtension) {
+        mDvbsStandardExtension = dvbsStandardExtension;
+        mDvbtStandardExtension = dvbtStandardExtension;
     }
 
     /**
@@ -50,11 +50,11 @@
      * @see android.media.tv.tuner.frontend.DvbsFrontendSettings
      */
     @DvbsFrontendSettings.Standard
-    public int getDvbsStandardExt() {
-        if (mDvbsStandardExt == FrontendDvbsStandard.UNDEFINED) {
+    public int getDvbsStandardExtension() {
+        if (mDvbsStandardExtension == FrontendDvbsStandard.UNDEFINED) {
             throw new IllegalStateException("No DVB-S standard transition");
         }
-        return mDvbsStandardExt;
+        return mDvbsStandardExtension;
     }
 
     /**
@@ -66,10 +66,10 @@
      * @see android.media.tv.tuner.frontend.DvbtFrontendSettings
      */
     @DvbtFrontendSettings.Standard
-    public int getDvbtStandardExt() {
-        if (mDvbtStandardExt == FrontendDvbtStandard.UNDEFINED) {
+    public int getDvbtStandardExtension() {
+        if (mDvbtStandardExtension == FrontendDvbtStandard.UNDEFINED) {
             throw new IllegalStateException("No DVB-T standard transition");
         }
-        return mDvbtStandardExt;
+        return mDvbtStandardExtension;
     }
 }
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 80ca4f2..2fe069a 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -2940,10 +2940,10 @@
                 break;
             }
             case FrontendStatus::Tag::standardExt: {
-                jfieldID field = env->GetFieldID(clazz, "mStandardExt",
-                        "Landroid/media/tv/tuner/frontend/StandardExt;");
+                jfieldID field = env->GetFieldID(clazz, "mStandardExtension",
+                        "Landroid/media/tv/tuner/frontend/StandardExtension;");
                 ScopedLocalRef standardExtClazz(env,
-                        env->FindClass("android/media/tv/tuner/frontend/StandardExt"));
+                        env->FindClass("android/media/tv/tuner/frontend/StandardExtension"));
                 jmethodID initStandardExt = env->GetMethodID(standardExtClazz.get(), "<init>",
                         "(II)V");
 
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 2d1fbf9..6b41ddd 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -363,6 +363,7 @@
     APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream
     APerformanceHint_notifyWorkloadIncrease; # introduced=36
     APerformanceHint_notifyWorkloadReset; # introduced=36
+    APerformanceHint_borrowSessionFromJava; # introduced=36
     AWorkDuration_create; # introduced=VanillaIceCream
     AWorkDuration_release; # introduced=VanillaIceCream
     AWorkDuration_setWorkPeriodStartTimestampNanos; # introduced=VanillaIceCream
@@ -383,6 +384,8 @@
     APerformanceHint_setUseFMQForTesting;
     APerformanceHint_getRateLimiterPropertiesForTesting;
     APerformanceHint_setUseNewLoadHintBehaviorForTesting;
+    APerformanceHint_closeSessionFromJava;
+    APerformanceHint_createSessionFromJava;
     extern "C++" {
         ASurfaceControl_registerSurfaceStatsListener*;
         ASurfaceControl_unregisterSurfaceStatsListener*;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index e2fa94d..bc1945e 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -36,6 +36,7 @@
 #include <cutils/trace.h>
 #include <fmq/AidlMessageQueue.h>
 #include <inttypes.h>
+#include <jni_wrappers.h>
 #include <performance_hint_private.h>
 #include <utils/SystemClock.h>
 
@@ -137,10 +138,14 @@
 
     APerformanceHintSession* createSession(const int32_t* threadIds, size_t size,
                                            int64_t initialTargetWorkDurationNanos,
-                                           hal::SessionTag tag = hal::SessionTag::APP);
+                                           hal::SessionTag tag = hal::SessionTag::APP,
+                                           bool isJava = false);
+    APerformanceHintSession* getSessionFromJava(JNIEnv* _Nonnull env, jobject _Nonnull sessionObj);
+
     int64_t getPreferredRateNanos() const;
     FMQWrapper& getFMQWrapper();
     bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex);
+    void initJava(JNIEnv* _Nonnull env);
 
 private:
     // Necessary to create an empty binder object
@@ -161,13 +166,16 @@
     FMQWrapper mFMQWrapper;
     double mHintBudget = kMaxLoadHintsPerInterval;
     int64_t mLastBudgetReplenish = 0;
+    bool mJavaInitialized = false;
+    jclass mJavaSessionClazz;
+    jfieldID mJavaSessionNativePtr;
 };
 
 struct APerformanceHintSession {
 public:
     APerformanceHintSession(std::shared_ptr<IHintManager> hintManager,
                             std::shared_ptr<IHintSession> session, int64_t preferredRateNanos,
-                            int64_t targetDurationNanos,
+                            int64_t targetDurationNanos, bool isJava,
                             std::optional<hal::SessionConfig> sessionConfig);
     APerformanceHintSession() = delete;
     ~APerformanceHintSession();
@@ -181,6 +189,7 @@
     int getThreadIds(int32_t* const threadIds, size_t* size);
     int setPreferPowerEfficiency(bool enabled);
     int reportActualWorkDuration(AWorkDuration* workDuration);
+    bool isJava();
 
 private:
     friend struct APerformanceHintManager;
@@ -203,6 +212,8 @@
     std::vector<int64_t> mLastHintSentTimestamp GUARDED_BY(sHintMutex);
     // Cached samples
     std::vector<hal::WorkDuration> mActualWorkDurations GUARDED_BY(sHintMutex);
+    // Is this session backing an SDK wrapper object
+    const bool mIsJava;
     std::string mSessionName;
     static int64_t sIDCounter GUARDED_BY(sHintMutex);
     // The most recent set of thread IDs
@@ -299,7 +310,7 @@
 
 APerformanceHintSession* APerformanceHintManager::createSession(
         const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos,
-        hal::SessionTag tag) {
+        hal::SessionTag tag, bool isJava) {
     std::vector<int32_t> tids(threadIds, threadIds + size);
     std::shared_ptr<IHintSession> session;
     ndk::ScopedAStatus ret;
@@ -312,7 +323,7 @@
         return nullptr;
     }
     auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
-                                           initialTargetWorkDurationNanos,
+                                           initialTargetWorkDurationNanos, isJava,
                                            sessionConfig.id == -1
                                                    ? std::nullopt
                                                    : std::make_optional<hal::SessionConfig>(
@@ -324,6 +335,18 @@
     return out;
 }
 
+APerformanceHintSession* APerformanceHintManager::getSessionFromJava(JNIEnv* env,
+                                                                     jobject sessionObj) {
+    initJava(env);
+    LOG_ALWAYS_FATAL_IF(!env->IsInstanceOf(sessionObj, mJavaSessionClazz),
+                        "Wrong java type passed to APerformanceHint_getSessionFromJava");
+    APerformanceHintSession* out = reinterpret_cast<APerformanceHintSession*>(
+            env->GetLongField(sessionObj, mJavaSessionNativePtr));
+    LOG_ALWAYS_FATAL_IF(out == nullptr, "Java-wrapped native hint session is nullptr");
+    LOG_ALWAYS_FATAL_IF(!out->isJava(), "Unmanaged native hint session returned from Java SDK");
+    return out;
+}
+
 int64_t APerformanceHintManager::getPreferredRateNanos() const {
     return mPreferredRateNanos;
 }
@@ -332,13 +355,23 @@
     return mFMQWrapper;
 }
 
+void APerformanceHintManager::initJava(JNIEnv* _Nonnull env) {
+    if (mJavaInitialized) {
+        return;
+    }
+    jclass sessionClazz = FindClassOrDie(env, "android/os/PerformanceHintManager$Session");
+    mJavaSessionClazz = MakeGlobalRefOrDie(env, sessionClazz);
+    mJavaSessionNativePtr = GetFieldIDOrDie(env, mJavaSessionClazz, "mNativeSessionPtr", "J");
+    mJavaInitialized = true;
+}
+
 // ===================================== APerformanceHintSession implementation
 
 constexpr int kNumEnums = enum_size<hal::SessionHint>();
 APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> hintManager,
                                                  std::shared_ptr<IHintSession> session,
                                                  int64_t preferredRateNanos,
-                                                 int64_t targetDurationNanos,
+                                                 int64_t targetDurationNanos, bool isJava,
                                                  std::optional<hal::SessionConfig> sessionConfig)
       : mHintManager(hintManager),
         mHintSession(std::move(session)),
@@ -347,6 +380,7 @@
         mFirstTargetMetTimestamp(0),
         mLastTargetMetTimestamp(0),
         mLastHintSentTimestamp(std::vector<int64_t>(kNumEnums, 0)),
+        mIsJava(isJava),
         mSessionConfig(sessionConfig) {
     if (sessionConfig->id > INT32_MAX) {
         ALOGE("Session ID too large, must fit 32-bit integer");
@@ -401,6 +435,10 @@
     return reportActualWorkDurationInternal(static_cast<AWorkDuration*>(&workDuration));
 }
 
+bool APerformanceHintSession::isJava() {
+    return mIsJava;
+}
+
 int APerformanceHintSession::sendHints(std::vector<hal::SessionHint>& hints, int64_t now,
                                        const char*) {
     std::scoped_lock lock(sHintMutex);
@@ -826,6 +864,22 @@
                                   static_cast<hal::SessionTag>(tag));
 }
 
+APerformanceHintSession* APerformanceHint_createSessionFromJava(
+        APerformanceHintManager* manager, const int32_t* threadIds, size_t size,
+        int64_t initialTargetWorkDurationNanos) {
+    VALIDATE_PTR(manager)
+    VALIDATE_PTR(threadIds)
+    return manager->createSession(threadIds, size, initialTargetWorkDurationNanos,
+                                  hal::SessionTag::APP, true);
+}
+
+APerformanceHintSession* APerformanceHint_borrowSessionFromJava(JNIEnv* env,
+                                                                    jobject sessionObj) {
+    VALIDATE_PTR(env)
+    VALIDATE_PTR(sessionObj)
+    return APerformanceHintManager::getInstance()->getSessionFromJava(env, sessionObj);
+}
+
 int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) {
     VALIDATE_PTR(manager)
     return manager->getPreferredRateNanos();
@@ -846,6 +900,16 @@
 
 void APerformanceHint_closeSession(APerformanceHintSession* session) {
     VALIDATE_PTR(session)
+    if (session->isJava()) {
+        LOG_ALWAYS_FATAL("%s: Java-owned PerformanceHintSession cannot be closed in native",
+                         __FUNCTION__);
+        return;
+    }
+    delete session;
+}
+
+void APerformanceHint_closeSessionFromJava(APerformanceHintSession* session) {
+    VALIDATE_PTR(session)
     delete session;
 }
 
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 9a7a39f..7ae2eafa 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -228,6 +228,10 @@
     field public static final String CATEGORY_PAYMENT = "payment";
     field public static final String EXTRA_CATEGORY = "category";
     field public static final String EXTRA_SERVICE_COMPONENT = "component";
+    field @FlaggedApi("android.nfc.nfc_event_listener") public static final int NFC_INTERNAL_ERROR_COMMAND_TIMEOUT = 3; // 0x3
+    field @FlaggedApi("android.nfc.nfc_event_listener") public static final int NFC_INTERNAL_ERROR_NFC_CRASH_RESTART = 1; // 0x1
+    field @FlaggedApi("android.nfc.nfc_event_listener") public static final int NFC_INTERNAL_ERROR_NFC_HARDWARE_ERROR = 2; // 0x2
+    field @FlaggedApi("android.nfc.nfc_event_listener") public static final int NFC_INTERNAL_ERROR_UNKNOWN = 0; // 0x0
     field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT = 3; // 0x3
     field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DH = 0; // 0x0
     field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE = 1; // 0x1
@@ -239,8 +243,13 @@
   }
 
   @FlaggedApi("android.nfc.nfc_event_listener") public static interface CardEmulation.NfcEventListener {
+    method @FlaggedApi("android.nfc.nfc_event_listener") public default void onAidConflictOccurred(@NonNull String);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public default void onAidNotRouted(@NonNull String);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public default void onInternalErrorReported(int);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public default void onNfcStateChanged(int);
     method @FlaggedApi("android.nfc.nfc_event_listener") public default void onObserveModeStateChanged(boolean);
     method @FlaggedApi("android.nfc.nfc_event_listener") public default void onPreferredServiceChanged(boolean);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public default void onRemoteFieldChanged(boolean);
   }
 
   public abstract class HostApduService extends android.app.Service {
diff --git a/nfc/java/android/nfc/INfcEventListener.aidl b/nfc/java/android/nfc/INfcEventListener.aidl
index 5162c26..774d8f8 100644
--- a/nfc/java/android/nfc/INfcEventListener.aidl
+++ b/nfc/java/android/nfc/INfcEventListener.aidl
@@ -8,4 +8,9 @@
 oneway interface INfcEventListener {
     void onPreferredServiceChanged(in ComponentNameAndUser ComponentNameAndUser);
     void onObserveModeStateChanged(boolean isEnabled);
+    void onAidConflictOccurred(in String aid);
+    void onAidNotRouted(in String aid);
+    void onNfcStateChanged(in int nfcState);
+    void onRemoteFieldChanged(boolean isDetected);
+    void onInternalErrorReported(in int errorType);
 }
\ No newline at end of file
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 8917524..e9ec721 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -1144,6 +1144,40 @@
         };
     }
 
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+    public static final int NFC_INTERNAL_ERROR_UNKNOWN = 0;
+
+    /**
+     * This error is reported when the NFC command watchdog restarts the NFC stack.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+    public static final int NFC_INTERNAL_ERROR_NFC_CRASH_RESTART = 1;
+
+    /**
+     * This error is reported when the NFC controller does not respond or there's an NCI transport
+     * error.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+    public static final int NFC_INTERNAL_ERROR_NFC_HARDWARE_ERROR = 2;
+
+    /**
+     * This error is reported when the NFC stack times out while waiting for a response to a command
+     * sent to the NFC hardware.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+    public static final int NFC_INTERNAL_ERROR_COMMAND_TIMEOUT = 3;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+    @IntDef(prefix = "NFC_INTERNAL_ERROR_", value = {
+            NFC_INTERNAL_ERROR_UNKNOWN,
+            NFC_INTERNAL_ERROR_NFC_CRASH_RESTART,
+            NFC_INTERNAL_ERROR_NFC_HARDWARE_ERROR,
+            NFC_INTERNAL_ERROR_COMMAND_TIMEOUT,
+    })
+    public @interface NfcInternalErrorType {}
+
     /** Listener for preferred service state changes. */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
     public interface NfcEventListener {
@@ -1166,6 +1200,57 @@
          */
         @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
         default void onObserveModeStateChanged(boolean isEnabled) {}
+
+        /**
+         * This method is called when an AID conflict is detected during an NFC transaction. This
+         * can happen when multiple services are registered for the same AID.
+         *
+         * @param aid The AID that is in conflict
+         */
+        @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+        default void onAidConflictOccurred(@NonNull String aid) {}
+
+        /**
+         * This method is called when an AID is not routed to any service during an NFC
+         * transaction. This can happen when no service is registered for the given AID.
+         *
+         * @param aid the AID that was not routed
+         */
+        @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+        default void onAidNotRouted(@NonNull String aid) {}
+
+        /**
+         * This method is called when the NFC state changes.
+         *
+         * @see NfcAdapter#getAdapterState()
+         *
+         * @param state The new NFC state
+         */
+        @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+        default void onNfcStateChanged(@NfcAdapter.AdapterState int state) {}
+        /**
+         * This method is called when the NFC controller is in card emulation mode and an NFC
+         * reader's field is either detected or lost.
+         *
+         * @param isDetected true if an NFC reader is detected, false if it is lost
+         */
+        @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+        default void onRemoteFieldChanged(boolean isDetected) {}
+
+        /**
+         * This method is called when an internal error is reported by the NFC stack.
+         *
+         * No action is required in response to these events as the NFC stack will automatically
+         * attempt to recover. These errors are reported for informational purposes only.
+         *
+         * Note that these errors can be reported when performing various internal NFC operations
+         * (such as during device shutdown) and cannot always be explicitly correlated with NFC
+         * transaction failures.
+         *
+         * @param errorType The type of the internal error
+         */
+        @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
+        default void onInternalErrorReported(@NfcInternalErrorType int errorType) {}
     }
 
     private final ArrayMap<NfcEventListener, Executor> mNfcEventListeners = new ArrayMap<>();
@@ -1185,25 +1270,61 @@
                                             mContext.getPackageName(),
                                             componentNameAndUser.getComponentName()
                                                     .getPackageName());
-                    synchronized (mNfcEventListeners) {
-                        mNfcEventListeners.forEach(
-                                (listener, executor) -> {
-                                    executor.execute(
-                                            () -> listener.onPreferredServiceChanged(isPreferred));
-                                });
-                    }
+                    callListeners(listener -> listener.onPreferredServiceChanged(isPreferred));
                 }
 
                 public void onObserveModeStateChanged(boolean isEnabled) {
                     if (!android.nfc.Flags.nfcEventListener()) {
                         return;
                     }
+                    callListeners(listener -> listener.onObserveModeStateChanged(isEnabled));
+                }
+
+                public void onAidConflictOccurred(String aid) {
+                    if (!android.nfc.Flags.nfcEventListener()) {
+                        return;
+                    }
+                    callListeners(listener -> listener.onAidConflictOccurred(aid));
+                }
+
+                public void onAidNotRouted(String aid) {
+                    if (!android.nfc.Flags.nfcEventListener()) {
+                        return;
+                    }
+                    callListeners(listener -> listener.onAidNotRouted(aid));
+                }
+
+                public void onNfcStateChanged(int state) {
+                    if (!android.nfc.Flags.nfcEventListener()) {
+                        return;
+                    }
+                    callListeners(listener -> listener.onNfcStateChanged(state));
+                }
+
+                public void onRemoteFieldChanged(boolean isDetected) {
+                    if (!android.nfc.Flags.nfcEventListener()) {
+                        return;
+                    }
+                    callListeners(listener -> listener.onRemoteFieldChanged(isDetected));
+                }
+
+                public void onInternalErrorReported(@NfcInternalErrorType int errorType) {
+                    if (!android.nfc.Flags.nfcEventListener()) {
+                        return;
+                    }
+                    callListeners(listener -> listener.onInternalErrorReported(errorType));
+                }
+
+                interface ListenerCall {
+                    void invoke(NfcEventListener listener);
+                }
+
+                private void callListeners(ListenerCall listenerCall) {
                     synchronized (mNfcEventListeners) {
                         mNfcEventListeners.forEach(
-                                (listener, executor) -> {
-                                    executor.execute(
-                                            () -> listener.onObserveModeStateChanged(isEnabled));
-                                });
+                            (listener, executor) -> {
+                                executor.execute(() -> listenerCall.invoke(listener));
+                            });
                     }
                 }
             };
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
index 43d7dfa..a3709c1 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
@@ -133,8 +133,11 @@
  */
 abstract class PreferenceLifecycleContext(context: Context) : ContextWrapper(context) {
 
-    /** Notifies that preference state is changed and update preference widget UI. */
-    abstract fun notifyPreferenceChange(preference: PreferenceMetadata)
+    /** Returns the preference widget object associated with given key. */
+    abstract fun <T> findPreference(key: String): T?
+
+    /** Notifies that preference state of given key is changed and updates preference widget UI. */
+    abstract fun notifyPreferenceChange(key: String)
 
     /**
      * Starts activity for result, see [android.app.Activity.startActivityForResult].
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index cfe6089..62ac3ad 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -57,8 +57,11 @@
 
     private val preferenceLifecycleContext =
         object : PreferenceLifecycleContext(context) {
-            override fun notifyPreferenceChange(preference: PreferenceMetadata) =
-                notifyChange(preference.key, CHANGE_REASON_STATE)
+            override fun <T> findPreference(key: String) =
+                preferenceScreen.findPreference(key) as T?
+
+            override fun notifyPreferenceChange(key: String) =
+                notifyChange(key, CHANGE_REASON_STATE)
 
             @Suppress("DEPRECATION")
             override fun startActivityForResult(
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index dc52b4d..b52ed42 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -1217,12 +1217,13 @@
                 }
             }
         }
+        Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, earliest group id = " + targetGroupId);
         return targetGroupId;
     }
 
     @Nullable
     private CachedBluetoothDevice getMainDevice(@Nullable List<BluetoothDevice> devices) {
-        if (devices == null || devices.size() == 1) return null;
+        if (devices == null || devices.isEmpty()) return null;
         List<CachedBluetoothDevice> cachedDevices =
                 devices.stream()
                         .map(device -> mDeviceManager.findDevice(device))
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index c90ba82..32d4580 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -464,5 +464,14 @@
         ));
         VALIDATORS.put(Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, ANY_STRING_VALIDATOR);
         VALIDATORS.put(Global.HEARING_DEVICE_LOCAL_NOTIFICATION, ANY_STRING_VALIDATOR);
+        VALIDATORS.put(
+                Global.Wearable.WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION,
+                new DiscreteValueValidator(
+                        new String[] {
+                                String.valueOf(
+                                        Global.Wearable.STATUS_TRAY_CONFIGURATION_DEFAULT),
+                                String.valueOf(
+                                        Global.Wearable.STATUS_TRAY_CONFIGURATION_SYSTEM_HIDDEN)
+                        }));
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 603a911..03fea37 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2434,28 +2434,40 @@
                 context.checkCallingOrSelfPermission(
                 Manifest.permission.WRITE_DEVICE_CONFIG)
                 == PackageManager.PERMISSION_GRANTED;
-        boolean isRoot = Binder.getCallingUid() == Process.ROOT_UID;
+        // Only the shell user and tests request the allowlist permission; this is used to force
+        // the WRITE_ALLOWLISTED_DEVICE_CONFIG path to log any flags that need to be allowlisted.
+        boolean isRestrictedShell = android.security.Flags.protectDeviceConfigFlags()
+                && hasAllowlistPermission;
 
-        if (isRoot) {
-            return;
-        }
-
-        if (hasWritePermission) {
+        if (!isRestrictedShell && hasWritePermission) {
             assertCallingUserDenyList(flags);
         } else if (hasAllowlistPermission) {
             for (String flag : flags) {
                 boolean namespaceAllowed = false;
-                for (String allowlistedPrefix : WritableNamespacePrefixes.ALLOWLIST) {
-                    if (flag.startsWith(allowlistedPrefix)) {
+                if (isRestrictedShell) {
+                    int delimiterIndex = flag.indexOf("/");
+                    String flagNamespace;
+                    if (delimiterIndex != -1) {
+                        flagNamespace = flag.substring(0, delimiterIndex);
+                    } else {
+                        flagNamespace = flag;
+                    }
+                    if (WritableNamespaces.ALLOWLIST.contains(flagNamespace)) {
                         namespaceAllowed = true;
-                        break;
+                    }
+                } else {
+                    for (String allowlistedPrefix : WritableNamespacePrefixes.ALLOWLIST) {
+                        if (flag.startsWith(allowlistedPrefix)) {
+                            namespaceAllowed = true;
+                            break;
+                        }
                     }
                 }
 
                 if (!namespaceAllowed && !DeviceConfig.getAdbWritableFlags().contains(flag)) {
-                    throw new SecurityException("Permission denial for flag '"
-                        + flag
-                        + "'; allowlist permission granted, but must add flag to the allowlist.");
+                    Slog.wtf(LOG_TAG, "Permission denial for flag '" + flag
+                            + "'; allowlist permission granted, but must add flag to the "
+                            + "allowlist");
                 }
             }
             assertCallingUserDenyList(flags);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java
new file mode 100644
index 0000000..d835c5f
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.settings;
+
+import android.util.ArraySet;
+
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Contains the list of namespaces in which any flag can be written by adb without root
+ * permissions.
+ * <p>
+ * A security review is required for any namespace that's added to this list. To add to
+ * the list, create a change and tag the OWNER. In the commit message, include a
+ * description of the flag's functionality, and a justification for why it needs to be
+ * allowlisted.
+ */
+final class WritableNamespaces {
+    public static final Set<String> ALLOWLIST =
+            new ArraySet<String>(Arrays.asList(
+                    "exo"
+            ));
+}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 9de7faf..a62b7fd 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -635,7 +635,8 @@
                     Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE,
                     Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED,
                     Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON,
-                    Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE);
+                    Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE,
+                    Settings.Global.Wearable.WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION);
 
     private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS =
              newHashSet(
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 526320d..c3cdb2b 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -179,6 +179,7 @@
     <uses-permission android:name="android.permission.SET_ORIENTATION" />
     <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.INSTALL_PACKAGE_UPDATES" />
+    <uses-permission android:name="android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES" />
     <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
     <uses-permission android:name="android.permission.INSTALL_DPC_PACKAGES" />
     <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" />
@@ -242,6 +243,8 @@
     <uses-permission android:name="android.permission.READ_LOWPAN_CREDENTIAL" />
     <uses-permission android:name="android.permission.BLUETOOTH_STACK" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.COPY_ACCOUNTS" />
+    <uses-permission android:name="android.permission.REMOVE_ACCOUNTS" />
     <uses-permission android:name="android.permission.RETRIEVE_WINDOW_TOKEN" />
     <uses-permission android:name="android.permission.FRAME_STATS" />
     <uses-permission android:name="android.permission.BIND_APPWIDGET" />
@@ -746,6 +749,9 @@
     <!-- Permission required for ATS test - CarDevicePolicyManagerTest -->
     <uses-permission android:name="android.permission.LOCK_DEVICE" />
 
+    <!-- Permission required for AuthenticationPolicyManagerTest -->
+    <uses-permission android:name="android.permission.MANAGE_SECURE_LOCK_DEVICE" />
+
     <!-- Permissions required for CTS test - CtsSafetyCenterTestCases -->
     <uses-permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
     <uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" />
@@ -956,10 +962,10 @@
     <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
         android:featureFlag="android.security.aapm_api"/>
 
-    <!-- Permission required for CTS test - ForensicManagerTest -->
-    <uses-permission android:name="android.permission.READ_FORENSIC_STATE"
+    <!-- Permission required for CTS test - IntrusionDetectionManagerTest -->
+    <uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"/>
-    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+    <uses-permission android:name="android.permission.MANAGE_INTRUSION_DETECTION_STATE"
         android:featureFlag="android.security.afl_api"/>
 
 
diff --git a/packages/Shell/aconfig/wear.aconfig b/packages/Shell/aconfig/wear.aconfig
index e07bd96..d88fd46 100644
--- a/packages/Shell/aconfig/wear.aconfig
+++ b/packages/Shell/aconfig/wear.aconfig
@@ -5,5 +5,5 @@
     name: "handle_bugreports_for_wear"
     namespace: "wear_services"
     description: "This flag enables Shell to propagate bugreport results to WearServices."
-    bug: "378060870"
+    bug: "338029043"
 }
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 67666c3d..ae168cd 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1780,6 +1780,16 @@
 }
 
 flag {
+    name: "notification_reentrant_dismiss"
+    namespace: "systemui"
+    description: "Posts to avoid a crashing reentrant pipeline run"
+    bug: "328328054"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+      }
+}
+
+flag {
     name: "stoppable_fgs_system_app"
     namespace: "systemui"
     description: "System app with foreground service can opt in to be stoppable."
@@ -1798,3 +1808,17 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "gsf_bouncer"
+    namespace: "systemui"
+    description: "Applies GSF font styles to Bouncer surfaces."
+    bug: "379364381"
+}
+
+flag {
+    name: "gsf_quick_settings"
+    namespace: "systemui"
+    description: "Applies GSF font styles to Quick Settings surfaces."
+    bug: "379364381"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index de4bdbc..e2bc409 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -114,16 +114,16 @@
             )
         }
 
-        internal fun assertReturnAnimations() {
+        fun assertReturnAnimations() {
             check(returnAnimationsEnabled()) {
                 "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag " +
                     "is disabled"
             }
         }
 
-        internal fun returnAnimationsEnabled() = returnAnimationFrameworkLibrary()
+        fun returnAnimationsEnabled() = returnAnimationFrameworkLibrary()
 
-        internal fun assertLongLivedReturnAnimations() {
+        fun assertLongLivedReturnAnimations() {
             check(longLivedReturnAnimationsEnabled()) {
                 "Long-lived registrations cannot be used when the " +
                     "returnAnimationFrameworkLibrary or the " +
@@ -131,7 +131,7 @@
             }
         }
 
-        internal fun longLivedReturnAnimationsEnabled() =
+        fun longLivedReturnAnimationsEnabled() =
             returnAnimationFrameworkLibrary() && returnAnimationFrameworkLongLived()
 
         internal fun WindowAnimationState.toTransitionState() =
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 9390664..597cbf2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -31,6 +31,7 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.customization.R as customR
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
@@ -147,7 +148,7 @@
             } else {
                 val scaleFactor = authController.scaleFactor
                 val bottomPaddingPx =
-                    context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
+                    context.resources.getDimensionPixelSize(customR.dimen.lock_icon_margin_bottom)
                 val heightPx = windowViewBounds.bottom.toFloat()
 
                 Pair(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 1603267..0db545f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -519,8 +519,19 @@
                 // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned
                 // true).
                 if (overSlop == 0f) {
-                    val delta = (drag.position - consumablePointer.position).toFloat()
-                    check(delta != 0f) { "delta is equal to 0" }
+                    // If the user drags in the opposite direction, the delta becomes zero because
+                    // we return to the original point. Therefore, we should use the previous event
+                    // to calculate the direction.
+                    val delta = (drag.position - drag.previousPosition).toFloat()
+                    check(delta != 0f) {
+                        buildString {
+                            append("delta is equal to 0 ")
+                            append("touchSlop ${currentValueOf(LocalViewConfiguration).touchSlop} ")
+                            append("consumablePointer.position ${consumablePointer.position} ")
+                            append("drag.position ${drag.position} ")
+                            append("drag.previousPosition ${drag.previousPosition}")
+                        }
+                    }
                     overSlop = delta.sign
                 }
                 drag
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index b3fd097..4bccac1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -430,14 +430,6 @@
                     check(transitionStates.size == 1)
                     check(transitionStates[0] is TransitionState.Idle)
                     transitionStates = listOf(transition)
-                } else if (currentState == transition.replacedTransition) {
-                    // Replace the transition.
-                    transitionStates =
-                        transitionStates.subList(0, transitionStates.lastIndex) + transition
-
-                    // Make sure it is removed from the finishedTransitions set if it was already
-                    // finished.
-                    finishedTransitions.remove(currentState)
                 } else {
                     // Append the new transition.
                     transitionStates = transitionStates + transition
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
index b87cc5c..3622369 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
@@ -132,6 +132,9 @@
         assertThat(state.currentTransitions)
             .comparingElementsUsing(FromToCurrentTriple)
             .containsExactly(
+                // Initial transition, A => B.
+                Triple(SceneA, SceneB, SceneB),
+
                 // Initial transition reversed, B back to A.
                 Triple(SceneA, SceneB, SceneA),
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index 5ec74f8..85d8b60 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -593,7 +593,7 @@
             }
         }
 
-        fun continueDraggingDown() {
+        fun dragDown() {
             rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
         }
 
@@ -603,11 +603,78 @@
         assertThat(started).isFalse()
 
         swipeConsume = true
-        continueDraggingDown()
+        // Drag in same direction
+        dragDown()
         assertThat(capturedChange).isNotNull()
         capturedChange = null
 
-        continueDraggingDown()
+        dragDown()
+        assertThat(capturedChange).isNull()
+
+        assertThat(started).isTrue()
+    }
+
+    @Test
+    fun multiPointerSwipeDetectorInteractionZeroOffsetFromStartPosition() {
+        val size = 200f
+        val middle = Offset(size / 2f, size / 2f)
+
+        var started = false
+
+        var capturedChange: PointerInputChange? = null
+        var swipeConsume = false
+
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(
+                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
+                    .nestedScrollDispatcher()
+                    .multiPointerDraggable(
+                        orientation = Orientation.Vertical,
+                        startDragImmediately = { false },
+                        swipeDetector =
+                            object : SwipeDetector {
+                                override fun detectSwipe(change: PointerInputChange): Boolean {
+                                    capturedChange = change
+                                    return swipeConsume
+                                }
+                            },
+                        onDragStarted = { _, _ ->
+                            started = true
+                            SimpleDragController(
+                                onDrag = { /* do nothing */ },
+                                onStop = { /* do nothing */ },
+                            )
+                        },
+                        dispatcher = defaultDispatcher,
+                    )
+            ) {}
+        }
+
+        fun startDraggingDown() {
+            rule.onRoot().performTouchInput {
+                down(middle)
+                moveBy(Offset(0f, touchSlop))
+            }
+        }
+
+        fun dragUp() {
+            rule.onRoot().performTouchInput { moveBy(Offset(0f, -touchSlop)) }
+        }
+
+        startDraggingDown()
+        assertThat(capturedChange).isNotNull()
+        capturedChange = null
+        assertThat(started).isFalse()
+
+        swipeConsume = true
+        // Drag in the opposite direction
+        dragUp()
+        assertThat(capturedChange).isNotNull()
+        capturedChange = null
+
+        dragUp()
         assertThat(capturedChange).isNull()
 
         assertThat(started).isTrue()
diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml b/packages/SystemUI/customization/res/values-land/dimens.xml
similarity index 66%
rename from libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml
rename to packages/SystemUI/customization/res/values-land/dimens.xml
index 4bd9ca0..50f220c8 100644
--- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml
+++ b/packages/SystemUI/customization/res/values-land/dimens.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
+<?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.
@@ -15,8 +14,6 @@
   ~ limitations under the License.
   -->
 
-<network-security-config>
-    <domain-config cleartextTrafficPermitted="true">
-        <domain includeSubdomains="true">localhost</domain>
-    </domain-config>
-</network-security-config>
+<resources>
+    <dimen name="lock_icon_margin_bottom">24dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml
index 18073ad..8760281 100644
--- a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml
@@ -17,4 +17,5 @@
 <resources>
     <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
     <dimen name="status_view_margin_horizontal">8dp</dimen>
+    <dimen name="lock_icon_margin_bottom">60dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml
index 041ae62..21b4c71 100644
--- a/packages/SystemUI/customization/res/values/dimens.xml
+++ b/packages/SystemUI/customization/res/values/dimens.xml
@@ -40,4 +40,5 @@
     <dimen name="date_weather_view_height">24dp</dimen>
     <dimen name="enhanced_smartspace_height">104dp</dimen>
     <dimen name="status_view_margin_horizontal">0dp</dimen>
+    <dimen name="lock_icon_margin_bottom">74dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
index 13f2c72..4e64c50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
@@ -17,22 +17,23 @@
 package com.android.systemui.biometrics.domain.interactor
 
 import android.graphics.Rect
-import android.hardware.fingerprint.FingerprintManager
 import android.view.MotionEvent
 import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.authController
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
+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.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -44,29 +45,25 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class UdfpsOverlayInteractorTest : SysuiTestCase() {
 
     @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
 
-    private lateinit var testScope: TestScope
+    private val kosmos = testKosmos()
 
-    @Mock private lateinit var fingerprintManager: FingerprintManager
-    @Mock private lateinit var authController: AuthController
+    private val testScope: TestScope = kosmos.testScope
+
+    private val authController: AuthController = kosmos.authController
     @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback>
 
     @Mock private lateinit var udfpsOverlayParams: UdfpsOverlayParams
     @Mock private lateinit var overlayBounds: Rect
-    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
 
     private lateinit var underTest: UdfpsOverlayInteractor
 
-    @Before
-    fun setUp() {
-        testScope = TestScope(StandardTestDispatcher())
-    }
-
     @Test
     fun testShouldInterceptTouch() =
         testScope.runTest {
@@ -107,15 +104,26 @@
             assertThat(udfpsOverlayParams()).isEqualTo(firstParams)
         }
 
+    @Test
+    fun testPaddingIsNeverNegative() =
+        testScope.runTest {
+            context.orCreateTestableResources.addOverride(R.dimen.pixel_pitch, 60.0f)
+            createUdfpsOverlayInteractor()
+            val padding by collectLastValue(underTest.iconPadding)
+            runCurrent()
+
+            verify(authController).addCallback(authControllerCallback.capture())
+
+            // Simulate the first empty udfps overlay params value.
+            authControllerCallback.value.onUdfpsLocationChanged(UdfpsOverlayParams())
+            runCurrent()
+
+            assertThat(padding).isEqualTo(0)
+            context.orCreateTestableResources.removeOverride(R.dimen.pixel_pitch)
+        }
+
     private fun createUdfpsOverlayInteractor() {
-        underTest =
-            UdfpsOverlayInteractor(
-                context,
-                authController,
-                selectedUserInteractor,
-                fingerprintManager,
-                testScope.backgroundScope
-            )
+        underTest = kosmos.udfpsOverlayInteractor
         testScope.runCurrent()
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 1e86516..e5f0d7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -19,13 +19,13 @@
 import android.os.UserHandle
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.uiEventLogger
 import com.android.internal.logging.uiEventLoggerFake
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
@@ -38,6 +38,7 @@
 import com.android.systemui.dock.dockManager
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -47,6 +48,8 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.notificationShadeWindowController
 import com.android.systemui.statusbar.phone.centralSurfaces
 import com.android.systemui.statusbar.phone.centralSurfacesOptional
@@ -65,12 +68,29 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @EnableFlags(FLAG_COMMUNAL_HUB)
-class CommunalSceneStartableTest : SysuiTestCase() {
+class CommunalSceneStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+    companion object {
+        private const val SCREEN_TIMEOUT = 1000
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     private val kosmos = testKosmos()
 
     private lateinit var underTest: CommunalSceneStartable
@@ -95,7 +115,6 @@
                         keyguardInteractor = keyguardInteractor,
                         systemSettings = fakeSettings,
                         notificationShadeWindowController = notificationShadeWindowController,
-                        featureFlagsClassic = kosmos.fakeFeatureFlagsClassic,
                         applicationScope = applicationCoroutineScope,
                         bgScope = applicationCoroutineScope,
                         mainDispatcher = testDispatcher,
@@ -114,7 +133,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun keyguardGoesAway_whenLaunchingEditMode_doNotForceBlankScene() =
         with(kosmos) {
             testScope.runTest {
@@ -135,7 +154,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun keyguardGoesAway_whenLaunchingWidget_doNotForceBlankScene() =
         with(kosmos) {
             testScope.runTest {
@@ -156,7 +175,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun keyguardGoesAway_whenNotLaunchingWidget_forceBlankScene() =
         with(kosmos) {
             testScope.runTest {
@@ -177,7 +196,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun keyguardGoesAway_whenInEditMode_doesNotChangeScene() =
         with(kosmos) {
             testScope.runTest {
@@ -198,6 +217,7 @@
 
     @Ignore("Ignored until custom animations are implemented in b/322787129")
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun deviceDocked_forceCommunalScene() =
         with(kosmos) {
             testScope.runTest {
@@ -215,7 +235,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun occluded_forceBlankScene() =
         with(kosmos) {
             testScope.runTest {
@@ -235,7 +255,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun occluded_doesNotForceBlankSceneIfLaunchingActivityOverLockscreen() =
         with(kosmos) {
             testScope.runTest {
@@ -255,7 +275,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -273,7 +293,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun deviceAsleep_forceBlankSceneAfterTimeout() =
         with(kosmos) {
             testScope.runTest {
@@ -295,7 +315,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
         with(kosmos) {
             testScope.runTest {
@@ -325,6 +345,7 @@
 
     @Ignore("Ignored until custom animations are implemented in b/322787129")
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun dockingOnLockscreen_forcesCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -347,6 +368,7 @@
 
     @Ignore("Ignored until custom animations are implemented in b/322787129")
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
         with(kosmos) {
             testScope.runTest {
@@ -377,6 +399,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_whenDreaming_goesToBlank() =
         with(kosmos) {
             testScope.runTest {
@@ -394,6 +417,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_notDreaming_staysOnCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -409,6 +433,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_dreamStopped_staysOnCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -432,6 +457,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_dreamStartedHalfway_goesToCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -454,6 +480,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_dreamAfterInitialTimeout_goesToBlank() =
         with(kosmos) {
             testScope.runTest {
@@ -474,6 +501,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_userActivityTriggered_resetsTimeout() =
         with(kosmos) {
             testScope.runTest {
@@ -501,6 +529,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun hubTimeout_screenTimeoutChanged() =
         with(kosmos) {
             testScope.runTest {
@@ -526,7 +555,163 @@
         }
 
     @Test
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_whenDreaming_goesToBlank() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is dreaming and on communal.
+                updateDreaming(true)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Scene times out back to blank after the screen timeout.
+                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Dream)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_notDreaming_staysOnCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is not dreaming and on communal.
+                updateDreaming(false)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                // Scene stays as Communal
+                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_dreamStopped_staysOnCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is dreaming and on communal.
+                updateDreaming(true)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Wait a bit, but not long enough to timeout.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Dream stops, timeout is cancelled and device stays on hub, because the regular
+                // screen timeout will take effect at this point.
+                updateDreaming(false)
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_dreamStartedHalfway_goesToCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is on communal, but not dreaming.
+                updateDreaming(false)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Wait a bit, but not long enough to timeout, then start dreaming.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                updateDreaming(true)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Device times out after one screen timeout interval, dream doesn't reset timeout.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Dream)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_dreamAfterInitialTimeout_goesToBlank() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is on communal.
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                // Device stays on the hub after the timeout since we're not dreaming.
+                advanceTimeBy(SCREEN_TIMEOUT.milliseconds * 2)
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Start dreaming.
+                updateDreaming(true)
+
+                // Hub times out immediately.
+                assertThat(scene).isEqualTo(Scenes.Dream)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_userActivityTriggered_resetsTimeout() =
+        with(kosmos) {
+            testScope.runTest {
+                // Device is dreaming and on communal.
+                updateDreaming(true)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Wait a bit, but not long enough to timeout.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+
+                // Send user interaction to reset timeout.
+                communalInteractor.signalUserInteraction()
+
+                // If user activity didn't reset timeout, we would have gone back to Blank by now.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Timeout happens one interval after the user interaction.
+                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Dream)
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    fun hubTimeout_withSceneContainer_screenTimeoutChanged() =
+        with(kosmos) {
+            testScope.runTest {
+                fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT * 2)
+
+                // Device is dreaming and on communal.
+                updateDreaming(true)
+                sceneInteractor.changeScene(Scenes.Communal, "test")
+
+                val scene by collectLastValue(sceneInteractor.currentScene)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                // Scene times out back to blank after the screen timeout.
+                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Communal)
+
+                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
+                assertThat(scene).isEqualTo(Scenes.Dream)
+                assertThat(uiEventLoggerFake.logs.first().eventId)
+                    .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT.id)
+                assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+            }
+        }
+
+    @Test
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_SCENE_CONTAINER)
     fun transitionFromDozingToGlanceableHub_forcesCommunal() =
         with(kosmos) {
             testScope.runTest {
@@ -558,8 +743,4 @@
             fakeKeyguardRepository.setDreaming(dreaming)
             runCurrent()
         }
-
-    companion object {
-        private const val SCREEN_TIMEOUT = 1000
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt
new file mode 100644
index 0000000..e17b66e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.development.data.repository
+
+import android.content.pm.UserInfo
+import android.os.Build
+import android.os.UserHandle
+import android.os.UserManager
+import android.os.userManager
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DevelopmentSettingRepositoryTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private val underTest = kosmos.developmentSettingRepository
+
+    @Test
+    fun nonAdminUser_unrestricted_neverDevelopmentEnabled() =
+        with(kosmos) {
+            testScope.runTest {
+                val userInfo = nonAdminUserInfo
+                val settingEnabled by
+                    collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo))
+
+                setUserRestriction(userInfo.userHandle, restricted = false)
+
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(false)
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(true)
+                assertThat(settingEnabled).isFalse()
+            }
+        }
+
+    @Test
+    fun nonAdminUser_restricted_neverDevelopmentEnabled() =
+        with(kosmos) {
+            testScope.runTest {
+                val userInfo = nonAdminUserInfo
+                val settingEnabled by
+                    collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo))
+
+                setUserRestriction(userInfo.userHandle, restricted = true)
+
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(false)
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(true)
+                assertThat(settingEnabled).isFalse()
+            }
+        }
+
+    @Test
+    fun adminUser_unrestricted_defaultValueOfSetting() =
+        with(kosmos) {
+            testScope.runTest {
+                val userInfo = adminUserInfo
+                val settingEnabled by
+                    collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo))
+
+                setUserRestriction(userInfo.userHandle, restricted = false)
+
+                val defaultValue = Build.TYPE == "eng"
+
+                assertThat(settingEnabled).isEqualTo(defaultValue)
+            }
+        }
+
+    @Test
+    fun adminUser_unrestricted_enabledTracksSetting() =
+        with(kosmos) {
+            testScope.runTest {
+                val userInfo = adminUserInfo
+                val settingEnabled by
+                    collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo))
+
+                setUserRestriction(userInfo.userHandle, restricted = false)
+
+                setSettingValue(false)
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(true)
+                assertThat(settingEnabled).isTrue()
+            }
+        }
+
+    @Test
+    fun adminUser_restricted_neverDevelopmentEnabled() =
+        with(kosmos) {
+            testScope.runTest {
+                val userInfo = adminUserInfo
+                val settingEnabled by
+                    collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo))
+
+                setUserRestriction(userInfo.userHandle, restricted = true)
+
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(false)
+                assertThat(settingEnabled).isFalse()
+
+                setSettingValue(true)
+                assertThat(settingEnabled).isFalse()
+            }
+        }
+
+    private companion object {
+        const val USER_RESTRICTION = UserManager.DISALLOW_DEBUGGING_FEATURES
+        const val SETTING_NAME = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED
+
+        val adminUserInfo =
+            UserInfo(
+                /* id= */ 10,
+                /* name= */ "",
+                /* flags */ UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
+            )
+        val nonAdminUserInfo =
+            UserInfo(/* id= */ 11, /* name= */ "", /* flags */ UserInfo.FLAG_FULL)
+
+        fun Kosmos.setUserRestriction(userHandle: UserHandle, restricted: Boolean) {
+            userManager.stub {
+                on { hasUserRestrictionForUser(eq(USER_RESTRICTION), eq(userHandle)) } doReturn
+                    restricted
+            }
+        }
+
+        fun Kosmos.setSettingValue(enabled: Boolean) {
+            fakeGlobalSettings.putInt(SETTING_NAME, if (enabled) 1 else 0)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.kt
new file mode 100644
index 0000000..f29dabe
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.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.systemui.development.domain.interactor
+
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.clipboardManager
+import android.content.pm.UserInfo
+import android.content.res.mainResources
+import android.os.Build
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.development.shared.model.BuildNumber
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BuildNumberInteractorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            fakeUserRepository.setUserInfos(listOf(adminUserInfo, nonAdminUserInfo))
+        }
+
+    private val expectedBuildNumber =
+        BuildNumber(
+            kosmos.mainResources.getString(
+                R.string.bugreport_status,
+                Build.VERSION.RELEASE_OR_CODENAME,
+                Build.ID,
+            )
+        )
+
+    private val clipLabel =
+        kosmos.mainResources.getString(
+            com.android.systemui.res.R.string.build_number_clip_data_label
+        )
+
+    private val underTest = kosmos.buildNumberInteractor
+
+    @Test
+    fun nonAdminUser_settingEnabled_buildNumberNull() =
+        with(kosmos) {
+            testScope.runTest {
+                val buildNumber by collectLastValue(underTest.buildNumber)
+
+                fakeUserRepository.setSelectedUserInfo(nonAdminUserInfo)
+                setSettingValue(true)
+
+                assertThat(buildNumber).isNull()
+            }
+        }
+
+    @Test
+    fun adminUser_buildNumberCorrect_onlyWhenSettingEnabled() =
+        with(kosmos) {
+            testScope.runTest {
+                val buildNumber by collectLastValue(underTest.buildNumber)
+
+                fakeUserRepository.setSelectedUserInfo(adminUserInfo)
+
+                setSettingValue(false)
+                assertThat(buildNumber).isNull()
+
+                setSettingValue(true)
+                assertThat(buildNumber).isEqualTo(expectedBuildNumber)
+            }
+        }
+
+    @Test
+    fun copyToClipboard() =
+        with(kosmos) {
+            testScope.runTest {
+                fakeUserRepository.setSelectedUserInfo(adminUserInfo)
+
+                underTest.copyBuildNumber()
+                runCurrent()
+
+                val argumentCaptor = argumentCaptor<ClipData>()
+
+                verify(clipboardManager).setPrimaryClip(argumentCaptor.capture())
+
+                with(argumentCaptor.firstValue) {
+                    assertThat(description.label).isEqualTo(clipLabel)
+                    assertThat(description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN))
+                        .isTrue()
+                    assertThat(itemCount).isEqualTo(1)
+                    assertThat(getItemAt(0).text).isEqualTo(expectedBuildNumber.value)
+                }
+            }
+        }
+
+    private companion object {
+        const val SETTING_NAME = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED
+
+        val adminUserInfo =
+            UserInfo(
+                /* id= */ 10,
+                /* name= */ "",
+                /* flags */ UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
+            )
+        val nonAdminUserInfo =
+            UserInfo(/* id= */ 11, /* name= */ "", /* flags */ UserInfo.FLAG_FULL)
+
+        fun Kosmos.setSettingValue(enabled: Boolean) {
+            fakeGlobalSettings.putInt(SETTING_NAME, if (enabled) 1 else 0)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index f0d79bb..47cba07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -568,6 +568,41 @@
             assertThat(isUnlocked).isFalse()
         }
 
+    @Test
+    fun lockNow() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(5000)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+            assertThat(isUnlocked).isTrue()
+
+            underTest.lockNow()
+            runCurrent()
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_fromSleepButton() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(5000)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            kosmos.powerInteractor.setAsleepForTest(
+                sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON
+            )
+            runCurrent()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
     private fun TestScope.unlockDevice() {
         val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
index c39c3fe..2d54337 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -43,6 +44,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.After
 import org.junit.Before
 import org.junit.runner.RunWith
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -84,6 +86,12 @@
     @Before
     fun setup() {
         underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
+        overrideResource(R.integer.udfps_padding_debounce_duration, 0)
+    }
+
+    @After
+    fun teardown() {
+        mContext.orCreateTestableResources.removeOverride(R.integer.udfps_padding_debounce_duration)
     }
 
     @Test
@@ -118,6 +126,7 @@
             runCurrent()
             assertThat(visible).isFalse()
         }
+
     fun fpNotRunning_overlayNotVisible() =
         testScope.runTest {
             val visible by collectLastValue(underTest.visible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index a65e7ed..bbfc960 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -1035,6 +1035,7 @@
         assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
     }
 
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     @Test
     fun testBouncerShown_setsLifecycleState() {
         val client = client
@@ -1067,6 +1068,39 @@
         assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
     }
 
+    @EnableFlags(FLAG_SCENE_CONTAINER)
+    @Test
+    fun testBouncerShown_withSceneContainer_setsLifecycleState() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*isPreview*/,
+            false, /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        // Bouncer shows.
+        kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "test")
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes from resumed back to started when the bouncer shows.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        // Bouncer closes.
+        kosmos.sceneInteractor.changeScene(Scenes.Dream, "test")
+        testScope.runCurrent()
+        mMainExecutor.runAllReady()
+
+        // Lifecycle state goes back to RESUMED.
+        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
     @Test
     @DisableFlags(FLAG_SCENE_CONTAINER)
     fun testCommunalVisible_setsLifecycleState() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
index aacfaed..82bcece 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 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.ToSplitShade
 import com.android.systemui.shade.data.repository.fakeShadeRepository
@@ -83,7 +82,6 @@
                 shadeMode = ShadeMode.Single,
             )
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
@@ -101,7 +99,6 @@
                 shadeMode = ShadeMode.Single,
             )
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
@@ -119,7 +116,6 @@
                 shadeMode = ShadeMode.Split,
             )
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
@@ -137,7 +133,6 @@
                 shadeMode = ShadeMode.Split,
             )
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
@@ -155,7 +150,6 @@
                 shadeMode = ShadeMode.Dual,
             )
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(
@@ -171,7 +165,6 @@
 
             setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
             assertThat(actions).isNotEmpty()
-            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
index 92764ae..74a0bafda 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -17,6 +17,10 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.app.IActivityTaskManager
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -25,8 +29,10 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.window.flags.Flags
 import com.android.wm.shell.keyguard.KeyguardTransitions
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.eq
@@ -41,6 +47,9 @@
 @RunWith(AndroidJUnit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
 class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
+
+    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
     private lateinit var underTest: WindowManagerLockscreenVisibilityManager
     private lateinit var executor: FakeExecutor
 
@@ -68,32 +77,62 @@
     }
 
     @Test
-    fun testLockscreenVisible_andAodVisible() {
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testLockscreenVisible_andAodVisible_without_keyguard_shell_transitions() {
         underTest.setLockscreenShown(true)
-        underTest.setAodVisible(true)
-
         verify(activityTaskManagerService).setLockScreenShown(true, false)
+        underTest.setAodVisible(true)
         verify(activityTaskManagerService).setLockScreenShown(true, true)
+
         verifyNoMoreInteractions(activityTaskManagerService)
     }
 
     @Test
-    fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible() {
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testLockscreenVisible_andAodVisible_with_keyguard_shell_transitions() {
         underTest.setLockscreenShown(true)
+        verify(keyguardTransitions).startKeyguardTransition(true, false)
         underTest.setAodVisible(true)
+        verify(keyguardTransitions).startKeyguardTransition(true, true)
 
+        verifyNoMoreInteractions(keyguardTransitions)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_without_keyguard_shell_transitions() {
+        underTest.setLockscreenShown(true)
         verify(activityTaskManagerService).setLockScreenShown(true, false)
+        underTest.setAodVisible(true)
         verify(activityTaskManagerService).setLockScreenShown(true, true)
+
         verifyNoMoreInteractions(activityTaskManagerService)
 
         underTest.setSurfaceBehindVisibility(true)
-
         verify(activityTaskManagerService).keyguardGoingAway(anyInt())
+
         verifyNoMoreInteractions(activityTaskManagerService)
     }
 
     @Test
-    fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway() {
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_with_keyguard_shell_transitions() {
+        underTest.setLockscreenShown(true)
+        verify(keyguardTransitions).startKeyguardTransition(true, false)
+        underTest.setAodVisible(true)
+        verify(keyguardTransitions).startKeyguardTransition(true, true)
+
+        verifyNoMoreInteractions(keyguardTransitions)
+
+        underTest.setSurfaceBehindVisibility(true)
+        verify(keyguardTransitions).startKeyguardTransition(false, false)
+
+        verifyNoMoreInteractions(keyguardTransitions)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_without_keyguard_shell_transitions() {
         underTest.setLockscreenShown(false)
         underTest.setAodVisible(false)
 
@@ -106,7 +145,22 @@
     }
 
     @Test
-    fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater() {
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_with_keyguard_shell_transitions() {
+        underTest.setLockscreenShown(false)
+        underTest.setAodVisible(false)
+
+        verify(keyguardTransitions).startKeyguardTransition(false, false)
+        verifyNoMoreInteractions(keyguardTransitions)
+
+        underTest.setSurfaceBehindVisibility(true)
+
+        verifyNoMoreInteractions(keyguardTransitions)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_without_keyguard_shell_transitions() {
         underTest.setAodVisible(false)
         verifyNoMoreInteractions(activityTaskManagerService)
 
@@ -116,7 +170,19 @@
     }
 
     @Test
-    fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall() {
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_with_keyguard_shell_transitions() {
+        underTest.setAodVisible(false)
+        verifyNoMoreInteractions(keyguardTransitions)
+
+        underTest.setLockscreenShown(true)
+        verify(keyguardTransitions).startKeyguardTransition(true, false)
+        verifyNoMoreInteractions(activityTaskManagerService)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_without_keyguard_shell_transitions() {
         underTest.setLockscreenShown(true)
         underTest.setSurfaceBehindVisibility(true)
         verify(activityTaskManagerService).keyguardGoingAway(0)
@@ -126,8 +192,27 @@
     }
 
     @Test
-    fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility() {
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_with_keyguard_shell_transitions() {
+        underTest.setLockscreenShown(true)
+        underTest.setSurfaceBehindVisibility(true)
+        verify(keyguardTransitions).startKeyguardTransition(false, false)
+
+        underTest.setSurfaceBehindVisibility(true)
+        verifyNoMoreInteractions(keyguardTransitions)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_without_keyguard_shell_transitions() {
         underTest.setSurfaceBehindVisibility(false)
         verify(activityTaskManagerService).setLockScreenShown(eq(true), any())
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
+    fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_with_keyguard_shell_transitions() {
+        underTest.setSurfaceBehindVisibility(false)
+        verify(keyguardTransitions).startKeyguardTransition(eq(true), any())
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt
index b0959e4..d42b538 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt
@@ -27,10 +27,13 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -43,6 +46,16 @@
     private val underTest: DeviceEntryForegroundViewModel =
         kosmos.deviceEntryForegroundIconViewModel
 
+    @Before
+    fun setup() {
+        context.orCreateTestableResources.addOverride(R.integer.udfps_padding_debounce_duration, 0)
+    }
+
+    @After
+    fun teardown() {
+        context.orCreateTestableResources.removeOverride(R.integer.udfps_padding_debounce_duration)
+    }
+
     @Test
     fun aodIconColorWhite() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
index 450aadd..ebc00c3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import android.graphics.drawable.Icon
 import android.os.UserHandle
+import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
 import kotlinx.coroutines.CoroutineDispatcher
@@ -33,14 +34,29 @@
 class FakeNoteTaskBubbleController(
     unUsed1: Context,
     unsUsed2: CoroutineDispatcher,
-    private val optionalBubbles: Optional<Bubbles>
+    private val optionalBubbles: Optional<Bubbles>,
 ) : NoteTaskBubblesController(unUsed1, unsUsed2) {
     override suspend fun areBubblesAvailable() = optionalBubbles.isPresent
 
-    override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) {
+    override suspend fun showOrHideAppBubble(
+        intent: Intent,
+        userHandle: UserHandle,
+        icon: Icon,
+        bubbleExpandBehavior: NoteTaskBubbleExpandBehavior,
+    ) {
         optionalBubbles.ifPresentOrElse(
-            { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
-            { throw IllegalAccessException() }
+            { bubbles ->
+                if (
+                    bubbleExpandBehavior == NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED &&
+                        bubbles.isBubbleExpanded(
+                            Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle)
+                        )
+                ) {
+                    return@ifPresentOrElse
+                }
+                bubbles.showOrHideAppBubble(intent, userHandle, icon)
+            },
+            { throw IllegalAccessException() },
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
index 9ef6b9c..e55d6ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
@@ -21,9 +21,9 @@
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService
+import com.android.systemui.res.R
 import com.android.wm.shell.bubbles.Bubbles
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
@@ -33,6 +33,9 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.whenever
 
 /** atest SystemUITests:NoteTaskBubblesServiceTest */
 @SmallTest
@@ -61,12 +64,40 @@
     }
 
     @Test
-    fun showOrHideAppBubble() {
+    fun showOrHideAppBubble_defaultExpandBehavior_shouldCallBubblesApi() {
         val intent = Intent()
         val user = UserHandle.SYSTEM
         val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
+        val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.DEFAULT
+        whenever(bubbles.isBubbleExpanded(any())).thenReturn(false)
 
-        createServiceBinder().showOrHideAppBubble(intent, user, icon)
+        createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
+
+        verify(bubbles).showOrHideAppBubble(intent, user, icon)
+    }
+
+    @Test
+    fun showOrHideAppBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() {
+        val intent = Intent().apply { setPackage("test") }
+        val user = UserHandle.SYSTEM
+        val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
+        val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED
+        whenever(bubbles.isBubbleExpanded(any())).thenReturn(true)
+
+        createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
+
+        verify(bubbles, never()).showOrHideAppBubble(intent, user, icon)
+    }
+
+    @Test
+    fun showOrHideAppBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() {
+        val intent = Intent().apply { setPackage("test") }
+        val user = UserHandle.SYSTEM
+        val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
+        val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED
+        whenever(bubbles.isBubbleExpanded(any())).thenReturn(false)
+
+        createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
 
         verify(bubbles).showOrHideAppBubble(intent, user, icon)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
index 8f4078b..d3578fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskEntryPoint.QS_NOTES_TILE
 import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -44,10 +45,19 @@
     }
 
     @Test
-    fun launchMode_keyguardUnlocked_launchModeAppBubble() {
+    fun launchMode_keyguardUnlocked_launchModeAppBubble_withDefaultExpandBehavior() {
         val underTest = DEFAULT_INFO.copy(isKeyguardLocked = false)
 
-        assertThat(underTest.launchMode).isEqualTo(NoteTaskLaunchMode.AppBubble)
+        assertThat(underTest.launchMode)
+            .isEqualTo(NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.DEFAULT))
+    }
+
+    @Test
+    fun launchMode_keyguardUnlocked_qsTileEntryPoint_launchModeAppBubble_withKeepIfExpandedExpandBehavior() {
+        val underTest = DEFAULT_INFO.copy(isKeyguardLocked = false, entryPoint = QS_NOTES_TILE)
+
+        assertThat(underTest.launchMode)
+            .isEqualTo(NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED))
     }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 2e074da..cca847e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -56,6 +56,7 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.flags.EnableSceneContainer
@@ -2532,6 +2533,146 @@
             assertThat(isAlternateBouncerVisible).isFalse()
         }
 
+    @Test
+    fun handleDeviceUnlockStatus_deviceLockedWhileOnDream_stayOnDream() =
+        testScope.runTest {
+            val transitionState =
+                prepareState(
+                    isDeviceUnlocked = false,
+                    initialSceneKey = Scenes.Lockscreen,
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                )
+            underTest.start()
+
+            val isUnlocked by
+                collectLastValue(
+                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
+                )
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            // Unlock device.
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+            // Change to Dream.
+            sceneInteractor.changeScene(Scenes.Dream, "test")
+            transitionState.value = ObservableTransitionState.Idle(Scenes.Dream)
+            runCurrent()
+            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene).isEqualTo(Scenes.Dream)
+
+            // Lock device, and verify stay on dream.
+            kosmos.fakeDeviceEntryRepository.deviceUnlockStatus.value =
+                DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null)
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Dream)
+        }
+
+    @Test
+    fun handleDeviceUnlockStatus_deviceLockedWhileOnCommunal_stayOnCommunal() =
+        testScope.runTest {
+            val transitionState =
+                prepareState(
+                    isDeviceUnlocked = false,
+                    initialSceneKey = Scenes.Lockscreen,
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                )
+            underTest.start()
+
+            val isUnlocked by
+                collectLastValue(
+                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
+                )
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            // Unlock device.
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+            // Change to Communal.
+            sceneInteractor.changeScene(Scenes.Communal, "test")
+            transitionState.value = ObservableTransitionState.Idle(Scenes.Communal)
+            runCurrent()
+            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene).isEqualTo(Scenes.Communal)
+
+            // Lock device, and verify stay on Communal.
+            kosmos.fakeDeviceEntryRepository.deviceUnlockStatus.value =
+                DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null)
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Communal)
+        }
+
+    @Test
+    fun replacesLockscreenSceneOnBackStack_whenFaceUnlocked_fromShade_noAlternateBouncer() =
+        testScope.runTest {
+            val transitionState =
+                prepareState(
+                    isDeviceUnlocked = false,
+                    initialSceneKey = Scenes.Lockscreen,
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                )
+            underTest.start()
+
+            val isUnlocked by
+                collectLastValue(
+                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
+                )
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val backStack by collectLastValue(sceneBackInteractor.backStack)
+            val isAlternateBouncerVisible by
+                collectLastValue(kosmos.alternateBouncerInteractor.isVisible)
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isAlternateBouncerVisible).isFalse()
+
+            // Change to shade.
+            sceneInteractor.changeScene(Scenes.Shade, "")
+            transitionState.value = ObservableTransitionState.Idle(Scenes.Shade)
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
+            assertThat(isAlternateBouncerVisible).isFalse()
+
+            // Show the alternate bouncer.
+            kosmos.alternateBouncerInteractor.forceShow()
+            kosmos.sysuiStatusBarStateController.leaveOpen = true // leave shade open
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
+            assertThat(isAlternateBouncerVisible).isTrue()
+
+            // Simulate race condition by hiding the alternate bouncer *before* the face unlock:
+            kosmos.alternateBouncerInteractor.hide()
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen)
+            assertThat(isAlternateBouncerVisible).isFalse()
+
+            // Trigger a face unlock.
+            updateFaceAuthStatus(isSuccess = true)
+            runCurrent()
+            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+            assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Gone)
+            assertThat(isAlternateBouncerVisible).isFalse()
+        }
+
     private fun TestScope.emulateSceneTransition(
         transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
         toScene: SceneKey,
@@ -2768,15 +2909,16 @@
     }
 
     private fun updateFaceAuthStatus(isSuccess: Boolean) {
-        if (isSuccess) {
-            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
-                SuccessFaceAuthenticationStatus(
-                    successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java)
-                )
-            )
-        } else {
-            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
-                FailedFaceAuthenticationStatus()
+        with(kosmos.fakeDeviceEntryFaceAuthRepository) {
+            isAuthenticated.value = isSuccess
+            setAuthenticationStatus(
+                if (isSuccess) {
+                    SuccessFaceAuthenticationStatus(
+                        successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java)
+                    )
+                } else {
+                    FailedFaceAuthenticationStatus()
+                }
             )
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 94a19c8..ed31f36 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -349,6 +349,7 @@
     @Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
     @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
     @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
+    @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
     protected final int mMaxUdfpsBurnInOffsetY = 5;
     protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
     protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
@@ -819,6 +820,7 @@
                 mNotificationShadeDepthController,
                 mShadeHeaderController,
                 mStatusBarTouchableRegionManager,
+                () -> mStatusBarLongPressGestureDetector,
                 mKeyguardStateController,
                 mKeyguardBypassController,
                 mScrimController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index ef132d5..61d4c99 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -126,6 +126,7 @@
     @Mock protected NotificationShadeDepthController mNotificationShadeDepthController;
     @Mock protected ShadeHeaderController mShadeHeaderController;
     @Mock protected StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    @Mock protected StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
     @Mock protected DozeParameters mDozeParameters;
     @Mock protected KeyguardStateController mKeyguardStateController;
     @Mock protected KeyguardBypassController mKeyguardBypassController;
@@ -250,6 +251,7 @@
                 mNotificationShadeDepthController,
                 mShadeHeaderController,
                 mStatusBarTouchableRegionManager,
+                () -> mStatusBarLongPressGestureDetector,
                 mKeyguardStateController,
                 mKeyguardBypassController,
                 mScrimController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
similarity index 69%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
index 4e7839e..af01547 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
@@ -21,7 +21,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.ShadePrimaryDisplayCommand
 import com.android.systemui.statusbar.commandline.commandRegistry
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -34,13 +36,16 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ShadeDisplaysRepositoryTest : SysuiTestCase() {
+class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val commandRegistry = kosmos.commandRegistry
+    private val displayRepository = kosmos.displayRepository
+    private val shadeDisplaysRepository = ShadeDisplaysRepositoryImpl()
     private val pw = PrintWriter(StringWriter())
 
-    private val underTest = ShadeDisplaysRepositoryImpl(commandRegistry)
+    private val underTest =
+        ShadePrimaryDisplayCommand(commandRegistry, displayRepository, shadeDisplaysRepository)
 
     @Before
     fun setUp() {
@@ -50,7 +55,7 @@
     @Test
     fun commandDisplayOverride_updatesDisplayId() =
         testScope.runTest {
-            val displayId by collectLastValue(underTest.displayId)
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
 
             val newDisplayId = 2
@@ -65,7 +70,7 @@
     @Test
     fun commandShadeDisplayOverride_resetsDisplayId() =
         testScope.runTest {
-            val displayId by collectLastValue(underTest.displayId)
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
 
             val newDisplayId = 2
@@ -78,4 +83,17 @@
             commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "reset"))
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
         }
+
+    @Test
+    fun commandShadeDisplayOverride_anyExternalDisplay_notOnDefaultAnymore() =
+        testScope.runTest {
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
+            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+            val newDisplayId = 2
+            displayRepository.addDisplay(displayId = newDisplayId)
+
+            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "any_external"))
+
+            assertThat(displayId).isEqualTo(newDisplayId)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
index fb7252b..60a1855 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar;
 
-import static android.app.Notification.CATEGORY_CALL;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertEquals;
@@ -199,19 +197,6 @@
     }
 
     @Test
-    public void testContentDescForNotification_noNotifContent() {
-        Notification n = new Notification.Builder(mContext, "test")
-                .setSmallIcon(0)
-                .setContentTitle("hello")
-                .setCategory(CATEGORY_CALL)
-                .build();
-        assertThat(NotificationContentDescription.contentDescForNotification(mContext, n)
-                .toString()).startsWith("com.android.systemui.tests notification");
-        assertThat(NotificationContentDescription.contentDescForNotification(mContext, n)
-                .toString()).doesNotContain("hello");
-    }
-
-    @Test
     @EnableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS})
     public void setIcon_withPreloaded_usesPreloaded() {
         Icon mockIcon = mock(Icon.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
index abb3e6e..0fbee6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
@@ -321,7 +321,7 @@
                 BaseHeadsUpManager.HeadsUpEntry.class);
         headsUpEntry.mEntry = notifEntry;
 
-        hum.onEntryRemoved(headsUpEntry);
+        hum.onEntryRemoved(headsUpEntry, "test");
 
         verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry));
     }
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 8ebdbaa..6175e05 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
@@ -82,7 +82,6 @@
     private val mJavaAdapter: JavaAdapter = JavaAdapter(testScope.backgroundScope)
 
     @Mock private lateinit var mShadeInteractor: ShadeInteractor
-
     @Mock private lateinit var dumpManager: DumpManager
     private lateinit var mAvalancheController: AvalancheController
 
@@ -205,6 +204,25 @@
         assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue()
     }
 
+    class TestAnimationStateHandler : AnimationStateHandler {
+        override fun setHeadsUpGoingAwayAnimationsAllowed(allowed: Boolean) {}
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testReorderingAllowed_clearsListOfEntriesToRemove() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(notifEntry)
+        assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue()
+
+        hmp.setAnimationStateHandler(TestAnimationStateHandler())
+        hmp.mOnReorderingAllowedListener.onReorderingAllowed()
+        assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.isEmpty()).isTrue()
+    }
+
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
     fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt
new file mode 100644
index 0000000..799ca4a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.volume.dialog.sliders.domain.interactor
+
+import android.app.ActivityManager
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+
+private val volumeDialogTimeout = 3.seconds
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class VolumeDialogSliderInputEventsInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: VolumeDialogSliderInputEventsInteractor
+
+    @Before
+    fun setup() {
+        underTest = kosmos.volumeDialogSliderInputEventsInteractor
+    }
+
+    @Test
+    fun inputEvents_resetDialogVisibilityTimeout() =
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                val dialogVisibility by
+                    collectLastValue(volumeDialogVisibilityInteractor.dialogVisibility)
+                fakeVolumeDialogController.onShowRequested(
+                    Events.SHOW_REASON_VOLUME_CHANGED,
+                    false,
+                    ActivityManager.LOCK_TASK_MODE_LOCKED,
+                )
+                runCurrent()
+                advanceTimeBy(volumeDialogTimeout / 2)
+                assertThat(dialogVisibility)
+                    .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
+
+                underTest.onTouchEvent(
+                    MotionEvent.obtain(
+                        /* downTime = */ 0,
+                        /* eventTime = */ 0,
+                        /* action = */ 0,
+                        /* x = */ 0f,
+                        /* y = */ 0f,
+                        /* metaState = */ 0,
+                    )
+                )
+                advanceTimeBy(volumeDialogTimeout / 2)
+
+                assertThat(dialogVisibility)
+                    .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt
index 7c5a487..3f995c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt
@@ -155,6 +155,30 @@
             }
         }
 
+    @Test
+    fun activeStreamChanges_showBoth() {
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                fakeVolumeDialogController.updateState {
+                    activeStream = AudioManager.STREAM_SYSTEM
+                    states.put(AudioManager.STREAM_MUSIC, buildStreamState())
+                    states.put(AudioManager.STREAM_SYSTEM, buildStreamState())
+                }
+                val slidersModel by collectLastValue(underTest.sliders)
+                runCurrent()
+
+                fakeVolumeDialogController.updateState { activeStream = AudioManager.STREAM_MUSIC }
+                runCurrent()
+
+                assertThat(slidersModel!!.slider)
+                    .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC))
+                assertThat(slidersModel!!.floatingSliders)
+                    .containsExactly(VolumeDialogSliderType.Stream(AudioManager.STREAM_SYSTEM))
+            }
+        }
+    }
+
     private fun buildStreamState(
         build: VolumeDialogController.StreamState.() -> Unit = {}
     ): VolumeDialogController.StreamState {
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 694357d..b8544a6 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -31,10 +31,10 @@
         app:layout_constraintBottom_toBottomOf="@id/volume_dialog_settings"
         app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container"
         app:layout_constraintStart_toStartOf="@id/volume_dialog_main_slider_container"
-        app:layout_constraintTop_toTopOf="@id/volume_ringer_and_drawer_container" />
+        app:layout_constraintTop_toTopOf="@id/volume_ringer_drawer" />
 
     <include
-        android:id="@id/volume_ringer_and_drawer_container"
+        android:id="@id/volume_ringer_drawer"
         layout="@layout/volume_ringer_drawer"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml
index dc6780a..38bb783 100644
--- a/packages/SystemUI/res/layout/volume_ringer_button.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_button.xml
@@ -14,6 +14,7 @@
   ~ limitations under the License.
   -->
 <FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content" >
 
@@ -25,6 +26,7 @@
         android:layout_marginBottom="@dimen/volume_dialog_components_spacing"
         android:contentDescription="@string/volume_ringer_mode"
         android:gravity="center"
+        android:tint="?androidprv:attr/materialColorOnSurface"
         android:src="@drawable/volume_ringer_item_bg"
         android:background="@drawable/volume_ringer_item_bg"/>
 
diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
index b71c470..d850bbe 100644
--- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
@@ -14,55 +14,18 @@
   ~ limitations under the License.
   -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/volume_ringer_and_drawer_container"
-    android:layout_width="wrap_content"
+<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/volume_ringer_drawer"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:clipChildren="false"
     android:clipToPadding="false"
     android:gravity="center"
-    android:layoutDirection="ltr">
+    android:layoutDirection="ltr"
+    android:orientation="vertical"
+    app:layoutDescription="@xml/volume_dialog_ringer_drawer_motion_scene">
 
-    <!-- Drawer view, invisible by default. -->
-    <FrameLayout
-        android:id="@+id/volume_drawer_container"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
+    <!-- add ringer buttons here -->
 
-        <!-- View that is animated to a tapped ringer selection, so it appears selected. -->
-        <FrameLayout
-            android:id="@+id/volume_drawer_selection_background"
-            android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size"
-            android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size"
-            android:layout_gravity="bottom|right"
-            android:alpha="0.0"
-            android:background="@drawable/volume_drawer_selection_bg" />
-
-        <LinearLayout
-            android:id="@+id/volume_drawer_options"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical">
-
-            <!-- add ringer buttons here -->
-
-        </LinearLayout>
-
-    </FrameLayout>
-
-    <!-- The current ringer selection. When the drawer is opened, this animates to the corresponding
-         position in the drawer. When the drawer is closed, it animates back. -->
-    <ImageButton
-        android:id="@+id/volume_new_ringer_active_button"
-        android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size"
-        android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size"
-        android:layout_marginBottom="@dimen/volume_dialog_components_spacing"
-        android:background="@drawable/volume_drawer_selection_bg"
-        android:contentDescription="@string/volume_ringer_change"
-        android:gravity="center"
-        android:src="@drawable/ic_volume_media"
-        android:tint="?androidprv:attr/materialColorOnPrimary" />
-
-</FrameLayout>
\ No newline at end of file
+</androidx.constraintlayout.motion.widget.MotionLayout>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 235015b..70ae5c1 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -91,7 +91,6 @@
     <dimen name="notification_blocker_channel_list_height">128dp</dimen>
 
     <dimen name="keyguard_indication_margin_bottom">8dp</dimen>
-    <dimen name="lock_icon_margin_bottom">24dp</dimen>
 
     <!-- Keyboard shortcuts helper -->
     <dimen name="ksh_container_horizontal_margin">48dp</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 75bee9f..055c3a6 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -20,7 +20,6 @@
     <!-- keyguard-->
     <dimen name="keyguard_indication_margin_bottom">25dp</dimen>
     <dimen name="ambient_indication_margin_bottom">115dp</dimen>
-    <dimen name="lock_icon_margin_bottom">60dp</dimen>
     <!-- margin from keyguard status bar to clock. For split shade it should be
          keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp -->
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 580f6d3..8cab155 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -948,7 +948,6 @@
     <dimen name="keyguard_translate_distance_on_swipe_up">-200dp</dimen>
 
     <dimen name="keyguard_indication_margin_bottom">32dp</dimen>
-    <dimen name="lock_icon_margin_bottom">74dp</dimen>
     <dimen name="ambient_indication_margin_bottom">71dp</dimen>
 
 
@@ -2057,6 +2056,9 @@
     <!-- UDFPS view attributes -->
     <!-- UDFPS icon size in microns/um -->
     <dimen name="udfps_icon_size" format="float">6000</dimen>
+    <!-- Limits the updates to at most one update per debounce duration to avoid too many
+         updates due to quick changes to padding.   -->
+    <integer name="udfps_padding_debounce_duration">100</integer>
     <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that
          relies on this value will not be sized correctly. -->
     <item name="pixel_pitch" format="float" type="dimen">-1</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 245ba0a..8a7f302 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3214,6 +3214,9 @@
     <!-- Text to display when copying the build number off QS [CHAR LIMIT=NONE]-->
     <string name="build_number_copy_toast">Build number copied to clipboard.</string>
 
+    <!-- Text for accessibility action for copying content to clipboard [CHAR LIMIT=NONE]-->
+    <string name="copy_to_clipboard_a11y_action">copy to clipboard.</string>
+
      <!-- Status for conversation without interaction data [CHAR LIMIT=120] -->
     <string name="basic_status">Open conversation</string>
     <!--Title text for Conversation widget set up screen [CHAR LIMIT=180] -->
@@ -3824,6 +3827,10 @@
          assigning a new custom key combination to a shortcut in shortcut helper. The helper is a
          component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
     <string name="shortcut_helper_customize_dialog_error_message">Key combination already in use. Try another key.</string>
+    <!-- Plus sign, used in keyboard shortcut helper to combine keys for shortcut. E.g. Ctrl + A
+         The helper is a component that shows the user which keyboard shortcuts they can use.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_plus_symbol">+</string>
 
 
     <!-- Keyboard touchpad tutorial scheduler-->
diff --git a/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml
new file mode 100644
index 0000000..877637e
--- /dev/null
+++ b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml
@@ -0,0 +1,49 @@
+<?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.
+  -->
+
+<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <Transition
+        android:id="@+id/transition"
+        app:constraintSetEnd="@+id/volume_dialog_ringer_drawer_open"
+        app:constraintSetStart="@+id/volume_dialog_ringer_drawer_close"
+        app:transitionEasing="path(0.05f, 0.7f, 0.1f, 1f)"
+        app:duration="400">
+    </Transition>
+
+    <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_close">
+        <Constraint
+            android:id="@+id/volume_ringer_drawer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+    </ConstraintSet>
+
+    <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_open">
+        <Constraint
+            android:id="@+id/volume_ringer_drawer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+    </ConstraintSet>
+
+</MotionScene>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 76af813..7d220b5 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -18,9 +18,11 @@
 
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.PictureInPictureSurfaceTransaction;
 import android.window.TaskSnapshot;
+import android.window.WindowAnimationState;
 
 import com.android.internal.os.IResultReceiver;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -107,6 +109,17 @@
     }
 
     /**
+     * @see IRecentsAnimationController#handOffAnimation
+     */
+    public void handOffAnimation(RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
+        try {
+            mAnimationController.handOffAnimation(targets, states);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to hand off animation", e);
+        }
+    }
+
+    /**
      * @see IRecentsAnimationController#detachNavigationBarFromApp
      */
     public void detachNavigationBarFromApp(boolean moveHomeToTop) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index abbbd73..9763295 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -36,8 +36,10 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlin.math.max
 
 /** Encapsulates business logic for interacting with the UDFPS overlay. */
 @SysUISingleton
@@ -124,8 +126,9 @@
         udfpsOverlayParams.map { params ->
             val sensorWidth = params.nativeSensorBounds.right - params.nativeSensorBounds.left
             val nativePadding = (sensorWidth - iconSize) / 2
-            (nativePadding * params.scaleFactor).toInt()
-        }
+            // padding can be negative when udfpsOverlayParams has not been initialized yet.
+            max(0, (nativePadding * params.scaleFactor).toInt())
+        }.distinctUntilChanged()
 
     companion object {
         private const val TAG = "UdfpsOverlayInteractor"
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 4f9443e..db9a7f5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -18,6 +18,7 @@
 
 import android.os.UserHandle
 import android.provider.Settings
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
 import com.android.internal.logging.UiEventLogger
@@ -28,6 +29,7 @@
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.shared.model.CommunalScenes.isCommunal
 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.dagger.SysUISingleton
@@ -35,7 +37,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dock.DockManager
-import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -62,7 +63,6 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onEach
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.withContext
 
 /**
@@ -83,7 +83,6 @@
     private val systemSettings: SystemSettings,
     centralSurfacesOpt: Optional<CentralSurfaces>,
     private val notificationShadeWindowController: NotificationShadeWindowController,
-    private val featureFlagsClassic: FeatureFlagsClassic,
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
@@ -168,7 +167,7 @@
                     communalInteractor.userActivity.emitOnStart(),
                 ) { scene, _ ->
                     // Only timeout if we're on the hub is open.
-                    scene == CommunalScenes.Communal
+                    scene.isCommunal()
                 }
                 .collectLatest { shouldTimeout ->
                     cancelHubTimeout()
@@ -182,7 +181,7 @@
                 .sample(communalSceneInteractor.currentScene, ::Pair)
                 .collectLatest { (isDreaming, scene) ->
                     this@CommunalSceneStartable.isDreaming = isDreaming
-                    if (scene == CommunalScenes.Communal && isDreaming && timeoutJob == null) {
+                    if (scene.isCommunal() && isDreaming && timeoutJob == null) {
                         // If dreaming starts after timeout has expired, ex. if dream restarts under
                         // the hub, just close the hub immediately.
                         communalSceneInteractor.changeScene(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index 428b83d..ae08f2b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -98,16 +98,17 @@
         transitionKey: TransitionKey? = null,
         keyguardState: KeyguardState? = null,
     ) {
-        if (SceneContainerFlag.isEnabled) {
-            return sceneInteractor.changeScene(
-                toScene = newScene.toSceneContainerSceneKey(),
-                loggingReason = loggingReason,
-                transitionKey = transitionKey,
-                sceneState = keyguardState,
-            )
-        }
-
         applicationScope.launch("$TAG#changeScene") {
+            if (SceneContainerFlag.isEnabled) {
+                sceneInteractor.changeScene(
+                    toScene = newScene.toSceneContainerSceneKey(),
+                    loggingReason = loggingReason,
+                    transitionKey = transitionKey,
+                    sceneState = keyguardState,
+                )
+                return@launch
+            }
+
             if (currentScene.value == newScene) return@launch
             logger.logSceneChangeRequested(
                 from = currentScene.value,
@@ -125,16 +126,17 @@
         newScene: SceneKey,
         loggingReason: String,
         delayMillis: Long = 0,
-        keyguardState: KeyguardState? = null
+        keyguardState: KeyguardState? = null,
     ) {
-        if (SceneContainerFlag.isEnabled) {
-            return sceneInteractor.snapToScene(
-                toScene = newScene.toSceneContainerSceneKey(),
-                loggingReason = loggingReason,
-            )
-        }
-
         applicationScope.launch("$TAG#snapToScene") {
+            if (SceneContainerFlag.isEnabled) {
+                sceneInteractor.snapToScene(
+                    toScene = newScene.toSceneContainerSceneKey(),
+                    loggingReason = loggingReason,
+                )
+                return@launch
+            }
+
             delay(delayMillis)
             if (currentScene.value == newScene) return@launch
             logger.logSceneChangeRequested(
@@ -161,10 +163,7 @@
         } else {
             repository.currentScene
                 .pairwiseBy(initialValue = repository.currentScene.value) { from, to ->
-                    logger.logSceneChangeCommitted(
-                        from = from,
-                        to = to,
-                    )
+                    logger.logSceneChangeCommitted(from = from, to = to)
                     to
                 }
                 .stateIn(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalScenes.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalScenes.kt
index e562dfc..7f15944 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalScenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalScenes.kt
@@ -18,9 +18,16 @@
 
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 
 /** Definition of the possible scenes for the communal UI. */
+@Deprecated(
+    "CommunalScenes are deprecated when SceneContainerFlag is enabled. " +
+        "Use com.android.systemui.scene.shared.model.Scenes instead. " +
+        "Use SceneKey.toSceneContainerSceneKey() to map legacy scenes to scene container scenes. " +
+        "Use SceneKey.isCommunal() to check whether a scene is a communal scene."
+)
 object CommunalScenes {
     /** The default scene, shows nothing and is only there to allow swiping to communal. */
     @JvmField val Blank = SceneKey("blank")
@@ -40,9 +47,8 @@
      * The rules are simple:
      * - A legacy communal scene maps to a communal scene in the Scene Transition Framework (STF).
      * - A legacy blank scene means that the communal scene layout does not render anything so
-     *   whatever is beneath the layout is shown. That usually means lockscreen or dream, both of
-     *   which are represented by the lockscreen scene in STF (but different keyguard states in
-     *   KTF).
+     *   whatever is beneath the layout is shown. That usually means lockscreen or dream, both in
+     *   STL are represented by the home scene family.
      */
     fun SceneKey.toSceneContainerSceneKey(): SceneKey {
         if (!isCommunalScene() || !SceneContainerFlag.isEnabled) {
@@ -51,8 +57,13 @@
 
         return when (this) {
             Communal -> Scenes.Communal
-            Blank -> Scenes.Lockscreen
+            Blank -> SceneFamilies.Home
             else -> throw Throwable("Unrecognized communal scene: $this")
         }
     }
+
+    /** Checks whether this is a communal scene based on whether [SceneContainerFlag] is enabled. */
+    fun SceneKey.isCommunal(): Boolean {
+        return if (SceneContainerFlag.isEnabled) this == Scenes.Communal else this == Communal
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt b/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt
new file mode 100644
index 0000000..a8fa979
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.development.data.repository
+
+import android.content.pm.UserInfo
+import android.os.Build
+import android.os.UserManager
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class DevelopmentSettingRepository
+@Inject
+constructor(
+    private val globalSettings: GlobalSettings,
+    private val userManager: UserManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+    private val settingFlow = globalSettings.observerFlow(SETTING)
+
+    /**
+     * Indicates whether development settings is enabled for this user. The conditions are:
+     * * Setting is enabled (defaults to true in eng builds)
+     * * User is an admin
+     * * User is not restricted from Debugging features.
+     */
+    fun isDevelopmentSettingEnabled(userInfo: UserInfo): Flow<Boolean> {
+        return settingFlow
+            .emitOnStart()
+            .map { checkDevelopmentSettingEnabled(userInfo) }
+            .flowOn(backgroundDispatcher)
+    }
+
+    private suspend fun checkDevelopmentSettingEnabled(userInfo: UserInfo): Boolean {
+        val hasUserRestriction =
+            withContext(backgroundDispatcher) {
+                userManager.hasUserRestrictionForUser(
+                    UserManager.DISALLOW_DEBUGGING_FEATURES,
+                    userInfo.userHandle,
+                )
+            }
+        val isSettingEnabled =
+            withContext(backgroundDispatcher) {
+                globalSettings.getInt(SETTING, DEFAULT_ENABLED) != 0
+            }
+        val isAdmin = userInfo.isAdmin
+        return isAdmin && !hasUserRestriction && isSettingEnabled
+    }
+
+    private companion object {
+        val DEFAULT_ENABLED = if (Build.TYPE == "eng") 1 else 0
+
+        const val SETTING = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt
new file mode 100644
index 0000000..4d786fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.development.domain.interactor
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.res.Resources
+import android.os.Build
+import android.os.UserHandle
+import com.android.internal.R as InternalR
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.development.data.repository.DevelopmentSettingRepository
+import com.android.systemui.development.shared.model.BuildNumber
+import com.android.systemui.res.R as SystemUIR
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.utils.UserScopedService
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class BuildNumberInteractor
+@Inject
+constructor(
+    repository: DevelopmentSettingRepository,
+    @Main resources: Resources,
+    private val userRepository: UserRepository,
+    private val clipboardManagerProvider: UserScopedService<ClipboardManager>,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+    /**
+     * Build number, or `null` if Development Settings is not enabled for the current user.
+     *
+     * @see DevelopmentSettingRepository.isDevelopmentSettingEnabled
+     */
+    val buildNumber: Flow<BuildNumber?> =
+        userRepository.selectedUserInfo
+            .flatMapConcat { userInfo -> repository.isDevelopmentSettingEnabled(userInfo) }
+            .map { enabled -> buildText.takeIf { enabled } }
+
+    private val buildText =
+        BuildNumber(
+            resources.getString(
+                InternalR.string.bugreport_status,
+                Build.VERSION.RELEASE_OR_CODENAME,
+                Build.ID,
+            )
+        )
+
+    private val clipLabel = resources.getString(SystemUIR.string.build_number_clip_data_label)
+
+    private val currentUserHandle: UserHandle
+        get() = userRepository.getSelectedUserInfo().userHandle
+
+    /**
+     * Copy to the clipboard the build number for the current user.
+     *
+     * This can be performed regardless of the current user having Development Settings enabled
+     */
+    suspend fun copyBuildNumber() {
+        withContext(backgroundDispatcher) {
+            clipboardManagerProvider
+                .forUser(currentUserHandle)
+                .setPrimaryClip(ClipData.newPlainText(clipLabel, buildText.value))
+        }
+    }
+}
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt
similarity index 84%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt
index a321fb0..5bd713f 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package com.android.systemui.development.shared.model
 
-/** {@hide} */
-parcelable ForensicEvent;
+@JvmInline value class BuildNumber(val value: String)
diff --git a/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt b/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt
new file mode 100644
index 0000000..72e1ced
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.development.ui.compose
+
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.Text
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.semantics
+import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+
+@Composable
+fun BuildNumber(
+    viewModelFactory: BuildNumberViewModel.Factory,
+    textColor: Color,
+    modifier: Modifier = Modifier,
+) {
+    val viewModel = rememberViewModel(traceName = "BuildNumber") { viewModelFactory.create() }
+
+    val buildNumber = viewModel.buildNumber
+
+    if (buildNumber != null) {
+        val haptics = LocalHapticFeedback.current
+        val copyToClipboardActionLabel = stringResource(id = R.string.copy_to_clipboard_a11y_action)
+
+        Text(
+            text = buildNumber.value,
+            modifier =
+                modifier
+                    .focusable()
+                    .wrapContentWidth()
+                    // Using this instead of combinedClickable because this node should not support
+                    // single click
+                    .pointerInput(Unit) {
+                        detectLongPressGesture {
+                            haptics.performHapticFeedback(HapticFeedbackType.LongPress)
+                            viewModel.onBuildNumberLongPress()
+                        }
+                    }
+                    .semantics {
+                        onLongClick(copyToClipboardActionLabel) {
+                            viewModel.onBuildNumberLongPress()
+                            true
+                        }
+                    }
+                    .basicMarquee(iterations = 1, initialDelayMillis = 2000)
+                    .minimumInteractiveComponentSize(),
+            color = textColor,
+            maxLines = 1,
+        )
+    } else {
+        Spacer(modifier)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt
new file mode 100644
index 0000000..68c51ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.development.ui.viewmodel
+
+import androidx.compose.runtime.getValue
+import com.android.systemui.development.domain.interactor.BuildNumberInteractor
+import com.android.systemui.development.shared.model.BuildNumber
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+/** View model for UI that (optionally) shows the build number and copies it on long press. */
+class BuildNumberViewModel
+@AssistedInject
+constructor(private val buildNumberInteractor: BuildNumberInteractor) : ExclusiveActivatable() {
+
+    private val hydrator = Hydrator("BuildNumberViewModel")
+
+    private val copyRequests = Channel<Unit>()
+
+    val buildNumber: BuildNumber? by
+        hydrator.hydratedStateOf(
+            traceName = "buildNumber",
+            initialValue = null,
+            source = buildNumberInteractor.buildNumber,
+        )
+
+    fun onBuildNumberLongPress() {
+        copyRequests.trySend(Unit)
+    }
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch { hydrator.activate() }
+            launch {
+                copyRequests.receiveAsFlow().collect { buildNumberInteractor.copyBuildNumber() }
+            }
+            awaitCancellation()
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): BuildNumberViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 1a56541..400d097 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.policy.IKeyguardDismissCallback
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -43,7 +44,6 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Hosts application business logic related to device entry.
@@ -174,6 +174,14 @@
     }
 
     /**
+     * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
+     * dismissed once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lockscreen.
+     */
+    val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+
+    /**
      * Attempt to enter the device and dismiss the lockscreen. If authentication is required to
      * unlock the device it will transition to bouncer.
      *
@@ -238,11 +246,8 @@
         isLockscreenEnabled()
     }
 
-    /**
-     * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
-     * dismissed once the authentication challenge is completed. For example, completing a biometric
-     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
-     * lockscreen.
-     */
-    val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+    /** Locks the device instantly. */
+    fun lockNow() {
+        deviceUnlockedInteractor.lockNow()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index 35eed5e..7d684ca 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -43,6 +43,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -57,6 +58,7 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -178,6 +180,8 @@
     val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> =
         repository.deviceUnlockStatus.asStateFlow()
 
+    private val lockNowRequests = Channel<Unit>()
+
     override suspend fun onActivated(): Nothing {
         authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
             if (!authMethod.isSecure) {
@@ -196,6 +200,11 @@
         awaitCancellation()
     }
 
+    /** Locks the device instantly. */
+    fun lockNow() {
+        lockNowRequests.trySend(Unit)
+    }
+
     private suspend fun handleLockAndUnlockEvents() {
         try {
             Log.d(TAG, "started watching for lock and unlock events")
@@ -225,10 +234,12 @@
                     .map { (isAsleep, lastSleepReason) ->
                         if (isAsleep) {
                             if (
-                                lastSleepReason == WakeSleepReason.POWER_BUTTON &&
+                                (lastSleepReason == WakeSleepReason.POWER_BUTTON) &&
                                     authenticationInteractor.getPowerButtonInstantlyLocks()
                             ) {
                                 LockImmediately("locked instantly from power button")
+                            } else if (lastSleepReason == WakeSleepReason.SLEEP_BUTTON) {
+                                LockImmediately("locked instantly from sleep button")
                             } else {
                                 LockWithDelay("entering sleep")
                             }
@@ -256,6 +267,7 @@
                         emptyFlow()
                     }
                 },
+                lockNowRequests.receiveAsFlow().map { LockImmediately("lockNow") },
             )
             .collectLatest(::onLockEvent)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index b330ba3..408fe83 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -47,6 +47,7 @@
 import androidx.lifecycle.ViewModelStore;
 
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
+import com.android.compose.animation.scene.SceneKey;
 import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
@@ -212,16 +213,14 @@
     private final Consumer<Boolean> mBouncerShowingConsumer = new Consumer<>() {
         @Override
         public void accept(Boolean bouncerShowing) {
-            mExecutor.execute(() -> {
-                if (mBouncerShowing == bouncerShowing) {
-                    return;
-                }
+            mExecutor.execute(() -> updateBouncerShowingLocked(bouncerShowing));
+        }
+    };
 
-                mBouncerShowing = bouncerShowing;
-
-                updateLifecycleStateLocked();
-                updateGestureBlockingLocked();
-            });
+    private final Consumer<SceneKey> mCurrentSceneConsumer = new Consumer<>() {
+        @Override
+        public void accept(SceneKey currentScene) {
+            mExecutor.execute(() -> updateBouncerShowingLocked(currentScene == Scenes.Bouncer));
         }
     };
 
@@ -425,8 +424,13 @@
                 mIsCommunalAvailableCallback));
         mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
                 mCommunalVisibleConsumer));
-        mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
-                mBouncerShowingConsumer));
+        if (SceneContainerFlag.isEnabled()) {
+            mFlows.add(collectFlow(getLifecycle(), sceneInteractor.getCurrentScene(),
+                    mCurrentSceneConsumer));
+        } else {
+            mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+                    mBouncerShowingConsumer));
+        }
     }
 
     @NonNull
@@ -707,4 +711,15 @@
         Log.w(TAG, "Removing dream overlay container view parent!");
         parentView.removeView(containerView);
     }
+
+    private void updateBouncerShowingLocked(boolean bouncerShowing) {
+        if (mBouncerShowing == bouncerShowing) {
+            return;
+        }
+
+        mBouncerShowing = bouncerShowing;
+
+        updateLifecycleStateLocked();
+        updateGestureBlockingLocked();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
index 5a9e52a..b37206a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -20,7 +20,6 @@
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -60,9 +59,6 @@
                                     if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
                                 add(Swipe.Up to bouncerOrGone)
 
-                                // "Home" is either Dream, Lockscreen, or Gone.
-                                add(Swipe.End to SceneFamilies.Home)
-
                                 addAll(
                                     when (shadeMode) {
                                         ShadeMode.Single -> singleShadeActions()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index e44bfe3..2cb822e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyboard.shortcut.ui
 
 import android.app.Dialog
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentHeight
@@ -33,6 +34,7 @@
 import com.android.systemui.statusbar.phone.create
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
 
 class ShortcutCustomizationDialogStarter
 @AssistedInject
@@ -48,7 +50,7 @@
         viewModel.shortcutCustomizationUiState.collect { uiState ->
             if (
                 uiState is ShortcutCustomizationUiState.AddShortcutDialog &&
-                    !uiState.isDialogShowing
+                !uiState.isDialogShowing
             ) {
                 dialog = createAddShortcutDialog().also { it.show() }
                 viewModel.onAddShortcutDialogShown()
@@ -57,6 +59,7 @@
                 dialog = null
             }
         }
+        awaitCancellation()
     }
 
     fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
@@ -66,14 +69,21 @@
     private fun createAddShortcutDialog(): Dialog {
         return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog
             ->
-            val uiState by viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle()
+            val uiState by
+                viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
+                    initialValue = ShortcutCustomizationUiState.Inactive
+                )
             AssignNewShortcutDialog(
                 uiState = uiState,
                 modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
                 onKeyPress = { viewModel.onKeyPressed(it) },
                 onCancel = { dialog.dismiss() },
             )
-            dialog.setOnDismissListener { viewModel.onAddShortcutDialogDismissed() }
+            dialog.setOnDismissListener { viewModel.onDialogDismissed() }
+
+            // By default, apps cannot intercept action key. The system always handles it. This
+            // flag is needed to enable customisation dialog window to intercept action key
+            dialog.window?.addPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index 43f0f20..d722933 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -24,6 +24,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -39,18 +40,22 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.onKeyEvent
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
 import com.android.systemui.res.R
@@ -81,15 +86,16 @@
             SelectedKeyCombinationContainer(
                 shouldShowErrorMessage = uiState.shouldShowErrorMessage,
                 onKeyPress = onKeyPress,
+                pressedKeys = uiState.pressedKeys,
             )
             KeyCombinationAlreadyInUseErrorMessage(uiState.shouldShowErrorMessage)
-            DialogButtons(onCancel, isValidKeyCombination = uiState.isValidKeyCombination)
+            DialogButtons(onCancel, isSetShortcutButtonEnabled = uiState.pressedKeys.isNotEmpty())
         }
     }
 }
 
 @Composable
-fun DialogButtons(onCancel: () -> Unit, isValidKeyCombination: Boolean) {
+fun DialogButtons(onCancel: () -> Unit, isSetShortcutButtonEnabled: Boolean) {
     Row(
         modifier =
             Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
@@ -113,7 +119,7 @@
             contentColor = MaterialTheme.colorScheme.onPrimary,
             text =
                 stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
-            enabled = isValidKeyCombination,
+            enabled = isSetShortcutButtonEnabled,
         )
     }
 }
@@ -137,10 +143,9 @@
 
 @Composable
 fun SelectedKeyCombinationContainer(
-    keyCombination: String =
-        stringResource(R.string.shortcut_helper_add_shortcut_dialog_placeholder),
     shouldShowErrorMessage: Boolean,
     onKeyPress: (KeyEvent) -> Boolean,
+    pressedKeys: List<ShortcutKey>,
 ) {
     val interactionSource = remember { MutableInteractionSource() }
     val isFocused by interactionSource.collectIsFocusedAsState()
@@ -148,6 +153,9 @@
         if (!isFocused) MaterialTheme.colorScheme.outline
         else if (shouldShowErrorMessage) MaterialTheme.colorScheme.error
         else MaterialTheme.colorScheme.primary
+    val focusRequester = remember { FocusRequester() }
+
+    LaunchedEffect(Unit) { focusRequester.requestFocus() }
 
     ClickableShortcutSurface(
         onClick = {},
@@ -157,22 +165,19 @@
             Modifier.padding(all = 16.dp)
                 .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
                 .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
-                .onPreviewKeyEvent { onKeyPress(it) },
+                .onKeyEvent { onKeyPress(it) }
+                .focusRequester(focusRequester),
         interactionSource = interactionSource,
     ) {
         Row(
             modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp),
             verticalAlignment = Alignment.CenterVertically,
         ) {
-            Text(
-                text = keyCombination,
-                style = MaterialTheme.typography.headlineSmall,
-                fontSize = 16.sp,
-                lineHeight = 24.sp,
-                fontWeight = FontWeight.W500,
-                color = MaterialTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.width(252.dp),
-            )
+            if (pressedKeys.isEmpty()) {
+                PressKeyPrompt()
+            } else {
+                PressedKeysTextContainer(pressedKeys)
+            }
             Spacer(modifier = Modifier.weight(1f))
             if (shouldShowErrorMessage) {
                 Icon(
@@ -187,6 +192,67 @@
 }
 
 @Composable
+private fun RowScope.PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) {
+    pressedKeys.forEachIndexed { keyIndex, key ->
+        if (keyIndex > 0) {
+            ShortcutKeySeparator()
+        }
+        if (key is ShortcutKey.Text) {
+            ShortcutTextKey(key)
+        } else if (key is ShortcutKey.Icon) {
+            ShortcutIconKey(key)
+        }
+    }
+}
+
+@Composable
+private fun ShortcutKeySeparator() {
+    Text(
+        text = stringResource(id = R.string.shortcut_helper_plus_symbol),
+        style = MaterialTheme.typography.titleSmall,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        color = MaterialTheme.colorScheme.onSurfaceVariant,
+    )
+}
+
+@Composable
+private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) {
+    Icon(
+        painter =
+            when (key) {
+                is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId)
+                is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable)
+            },
+        contentDescription = null,
+        modifier = Modifier.align(Alignment.CenterVertically).height(24.dp),
+        tint = MaterialTheme.colorScheme.onSurfaceVariant,
+    )
+}
+
+@Composable
+private fun PressKeyPrompt() {
+    Text(
+        text = stringResource(id = R.string.shortcut_helper_add_shortcut_dialog_placeholder),
+        style = MaterialTheme.typography.titleSmall,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        color = MaterialTheme.colorScheme.onSurfaceVariant,
+    )
+}
+
+@Composable
+private fun ShortcutTextKey(key: ShortcutKey.Text) {
+    Text(
+        text = key.value,
+        style = MaterialTheme.typography.titleSmall,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        color = MaterialTheme.colorScheme.onSurfaceVariant,
+    )
+}
+
+@Composable
 private fun Title(title: String, modifier: Modifier = Modifier) {
     Text(
         text = title,
@@ -203,8 +269,6 @@
     Text(
         text = stringResource(id = R.string.shortcut_helper_customize_mode_sub_title),
         style = MaterialTheme.typography.bodyMedium,
-        fontSize = 14.sp,
-        lineHeight = 20.sp,
         modifier = modifier.wrapContentSize(Alignment.Center),
         color = MaterialTheme.colorScheme.onSurfaceVariant,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
index e9f2a3b..552c53d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
@@ -22,9 +22,9 @@
     data class AddShortcutDialog(
         val shortcutLabel: String,
         val shouldShowErrorMessage: Boolean,
-        val isValidKeyCombination: Boolean,
         val defaultCustomShortcutModifierKey: ShortcutKey.Icon.ResIdIcon,
         val isDialogShowing: Boolean,
+        val pressedKeys: List<ShortcutKey> = emptyList(),
     ) : ShortcutCustomizationUiState
 
     data object Inactive : ShortcutCustomizationUiState
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index e86da5d..2455ce4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -17,14 +17,22 @@
 package com.android.systemui.keyboard.shortcut.ui.viewmodel
 
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.isMetaPressed
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.input.key.type
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor
+import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.update
 
 class ShortcutCustomizationViewModel
@@ -35,7 +43,21 @@
     private val _shortcutCustomizationUiState =
         MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)
 
-    val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow()
+    val shortcutCustomizationUiState =
+        shortcutCustomizationInteractor.pressedKeys
+            .map { keys ->
+                // Note that Action Key is excluded as it's already displayed on the UI
+                keys.filter {
+                    it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey()
+                }
+            }
+            .combine(_shortcutCustomizationUiState) { keys, uiState ->
+                if (uiState is ShortcutCustomizationUiState.AddShortcutDialog) {
+                    uiState.copy(pressedKeys = keys)
+                } else {
+                    uiState
+                }
+            }
 
     fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
         when (requestInfo) {
@@ -44,10 +66,10 @@
                     ShortcutCustomizationUiState.AddShortcutDialog(
                         shortcutLabel = requestInfo.label,
                         shouldShowErrorMessage = false,
-                        isValidKeyCombination = false,
                         defaultCustomShortcutModifierKey =
                             shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(),
                         isDialogShowing = false,
+                        pressedKeys = emptyList(),
                     )
                 _shortcutBeingCustomized.value = requestInfo
             }
@@ -62,18 +84,48 @@
         }
     }
 
-    fun onAddShortcutDialogDismissed() {
+    fun onDialogDismissed() {
         _shortcutBeingCustomized.value = null
         _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive
+        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null)
     }
 
     fun onKeyPressed(keyEvent: KeyEvent): Boolean {
-        // TODO Not yet implemented b/373638584
+        if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) {
+            updatePressedKeys(keyEvent)
+            return true
+        }
         return false
     }
 
+    private fun updatePressedKeys(keyEvent: KeyEvent) {
+        val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key)
+        val keyCombination =
+            KeyCombination(
+                modifiers = keyEvent.nativeKeyEvent.modifiers,
+                keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null,
+            )
+        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination)
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): ShortcutCustomizationViewModel
     }
+
+    companion object {
+        private val SUPPORTED_MODIFIERS =
+            listOf(
+                Key.MetaLeft,
+                Key.MetaRight,
+                Key.CtrlRight,
+                Key.CtrlLeft,
+                Key.AltLeft,
+                Key.AltRight,
+                Key.ShiftLeft,
+                Key.ShiftRight,
+                Key.Function,
+                Key.Symbol,
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 32c2bc7..7097c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -662,7 +662,9 @@
             trace("doKeyguardTimeout");
             checkPermission();
 
-            if (KeyguardWmStateRefactor.isEnabled()) {
+            if (SceneContainerFlag.isEnabled()) {
+                mDeviceEntryInteractorLazy.get().lockNow();
+            } else if (KeyguardWmStateRefactor.isEnabled()) {
                 mKeyguardLockWhileAwakeInteractor.onKeyguardServiceDoKeyguardTimeout(options);
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index f473a82..364b1a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -202,30 +202,6 @@
                 .distinctUntilChanged()
         }
 
-    /**
-     * Scenes that are part of the keyguard and are shown when the device is locked or when the
-     * keyguard still needs to be dismissed.
-     */
-    private val keyguardScenes = setOf(Scenes.Lockscreen, Scenes.Bouncer, Scenes.Communal)
-
-    /**
-     * Scenes that don't belong in the keyguard family and cannot show when the device is locked or
-     * when the keyguard still needs to be dismissed.
-     */
-    private val nonKeyguardScenes = setOf(Scenes.Gone)
-
-    /**
-     * Scenes that can show regardless of device lock or keyguard dismissal states. Other sources of
-     * state need to be consulted to know whether the device has been entered or not.
-     */
-    private val keyguardAgnosticScenes =
-        setOf(
-            Scenes.Shade,
-            Scenes.QuickSettings,
-            Overlays.NotificationsShade,
-            Overlays.QuickSettingsShade,
-        )
-
     private val lockscreenVisibilityWithScenes =
         combine(
                 sceneInteractor.get().transitionState.flatMapLatestConflated {
@@ -355,4 +331,30 @@
                     !BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode)
             }
             .distinctUntilChanged()
+
+    companion object {
+        /**
+         * Scenes that are part of the keyguard and are shown when the device is locked or when the
+         * keyguard still needs to be dismissed.
+         */
+        val keyguardScenes = setOf(Scenes.Lockscreen, Scenes.Bouncer, Scenes.Communal, Scenes.Dream)
+
+        /**
+         * Scenes that don't belong in the keyguard family and cannot show when the device is locked
+         * or when the keyguard still needs to be dismissed.
+         */
+        private val nonKeyguardScenes = setOf(Scenes.Gone)
+
+        /**
+         * Scenes that can show regardless of device lock or keyguard dismissal states. Other
+         * sources of state need to be consulted to know whether the device has been entered or not.
+         */
+        private val keyguardAgnosticScenes =
+            setOf(
+                Scenes.Shade,
+                Scenes.QuickSettings,
+                Overlays.NotificationsShade,
+                Overlays.QuickSettingsShade,
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index b51bb7b..aa7eb29 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -28,6 +28,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.customization.R as customR
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -115,7 +116,7 @@
 
         val scaleFactor: Float = authController.scaleFactor
         val mBottomPaddingPx =
-            context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
+            context.resources.getDimensionPixelSize(customR.dimen.lock_icon_margin_bottom)
         val bounds = windowManager.currentWindowMetrics.bounds
         var widthPixels = bounds.right.toFloat()
         if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
index 5065fcb..1965252 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
@@ -31,8 +31,10 @@
 import javax.inject.Inject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
@@ -40,6 +42,7 @@
 import kotlinx.coroutines.flow.onStart
 
 /** Models the UI state for the device entry icon foreground view (displayed icon). */
+@OptIn(FlowPreview::class)
 @ExperimentalCoroutinesApi
 @SysUISingleton
 class DeviceEntryForegroundViewModel
@@ -97,7 +100,7 @@
     private val padding: Flow<Int> =
         deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported ->
             if (udfpsSupported) {
-                udfpsOverlayInteractor.iconPadding
+                udfpsOverlayInteractor.iconPadding.debounce(udfpsPaddingDebounceDuration.toLong())
             } else {
                 configurationInteractor.scaleForResolution.map { scale ->
                     (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
@@ -120,6 +123,9 @@
             )
         }
 
+    private val udfpsPaddingDebounceDuration: Int
+        get() = context.resources.getInteger(R.integer.udfps_padding_debounce_duration)
+
     data class ForegroundIconViewModel(
         val type: DeviceEntryIconView.IconType,
         val useAodVariant: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
index 3e947d9..7803f22 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
+++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
@@ -16,6 +16,7 @@
 
 package com.android.systemui.notetask;
 
+import com.android.systemui.notetask.NoteTaskBubbleExpandBehavior;
 import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.os.UserHandle;
@@ -25,5 +26,6 @@
 
     boolean areBubblesAvailable();
 
-    void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon);
+    void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon,
+     in NoteTaskBubbleExpandBehavior bubbleExpandBehavior);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl
new file mode 100644
index 0000000..86a06a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl
@@ -0,0 +1,3 @@
+package com.android.systemui.notetask;
+
+parcelable NoteTaskBubbleExpandBehavior;
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt
new file mode 100644
index 0000000..63b38a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.notetask
+
+import android.os.Parcel
+import android.os.Parcelable
+
+enum class NoteTaskBubbleExpandBehavior : Parcelable {
+    /**
+     * The default bubble expand behavior for note task bubble: The bubble will collapse if there is
+     * already an expanded bubble, The bubble will expand if there is a collapsed bubble.
+     */
+    DEFAULT,
+    /**
+     * The special bubble expand behavior for note task bubble: The bubble will stay expanded, not
+     * collapse, if there is already an expanded bubble, The bubble will expand if there is a
+     * collapsed bubble.
+     */
+    KEEP_IF_EXPANDED;
+
+    override fun describeContents(): Int {
+        return 0
+    }
+
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        dest.writeString(name)
+    }
+
+    companion object CREATOR : Parcelable.Creator<NoteTaskBubbleExpandBehavior> {
+        override fun createFromParcel(parcel: Parcel?): NoteTaskBubbleExpandBehavior {
+            return parcel?.readString()?.let { valueOf(it) } ?: DEFAULT
+        }
+
+        override fun newArray(size: Int) = arrayOfNulls<NoteTaskBubbleExpandBehavior>(size)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
index ec205f8..169285f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.DebugLogger.debugLog
+import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
 import javax.inject.Inject
@@ -48,7 +49,7 @@
 @Inject
 constructor(
     @Application private val context: Context,
-    @Background private val bgDispatcher: CoroutineDispatcher
+    @Background private val bgDispatcher: CoroutineDispatcher,
 ) {
 
     private val serviceConnector: ServiceConnector<INoteTaskBubblesService> =
@@ -57,7 +58,7 @@
             Intent(context, NoteTaskBubblesService::class.java),
             Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
             UserHandle.USER_SYSTEM,
-            INoteTaskBubblesService.Stub::asInterface
+            INoteTaskBubblesService.Stub::asInterface,
         )
 
     /** Returns whether notes app bubble is supported. */
@@ -79,11 +80,12 @@
     open suspend fun showOrHideAppBubble(
         intent: Intent,
         userHandle: UserHandle,
-        icon: Icon
+        icon: Icon,
+        bubbleExpandBehavior: NoteTaskBubbleExpandBehavior,
     ) {
         withContext(bgDispatcher) {
             serviceConnector
-                .post { it.showOrHideAppBubble(intent, userHandle, icon) }
+                .post { it.showOrHideAppBubble(intent, userHandle, icon, bubbleExpandBehavior) }
                 .whenComplete { _, error ->
                     if (error != null) {
                         debugLog(error = error) {
@@ -120,16 +122,28 @@
                 override fun showOrHideAppBubble(
                     intent: Intent,
                     userHandle: UserHandle,
-                    icon: Icon
+                    icon: Icon,
+                    bubbleExpandBehavior: NoteTaskBubbleExpandBehavior,
                 ) {
                     mOptionalBubbles.ifPresentOrElse(
-                        { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
+                        { bubbles ->
+                            if (
+                                bubbleExpandBehavior ==
+                                    NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED &&
+                                    bubbles.isBubbleExpanded(
+                                        Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle)
+                                    )
+                            ) {
+                                return@ifPresentOrElse
+                            }
+                            bubbles.showOrHideAppBubble(intent, userHandle, icon)
+                        },
                         {
                             debugLog {
                                 "Failed to show or hide bubble for intent $intent," +
                                     "user $user, and icon $icon as bubble is empty."
                             }
-                        }
+                        },
                     )
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 1fa5baa..a615963 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -84,7 +84,7 @@
     private val userTracker: UserTracker,
     private val secureSettings: SecureSettings,
     @Application private val applicationScope: CoroutineScope,
-    @Background private val bgCoroutineContext: CoroutineContext
+    @Background private val bgCoroutineContext: CoroutineContext,
 ) {
 
     @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>()
@@ -98,7 +98,7 @@
         if (key != Bubble.getAppBubbleKeyForApp(info.packageName, info.user)) return
 
         // Safe guard mechanism, this callback should only be called for app bubbles.
-        if (info.launchMode != NoteTaskLaunchMode.AppBubble) return
+        if (info.launchMode !is NoteTaskLaunchMode.AppBubble) return
 
         if (isExpanding) {
             debugLog { "onBubbleExpandChanged - expanding: $info" }
@@ -117,10 +117,8 @@
             } else {
                 getUserForHandlingNotesTaking(entryPoint)
             }
-        activityContext.startActivityAsUser(
-            createNotesRoleHolderSettingsIntent(),
-            user
-        )
+
+        activityContext.startActivityAsUser(createNotesRoleHolderSettingsIntent(), user)
     }
 
     /**
@@ -140,8 +138,7 @@
                 entryPoint == QUICK_AFFORDANCE -> {
                 userTracker.userProfiles
                     .firstOrNull { userManager.isManagedProfile(it.id) }
-                    ?.userHandle
-                    ?: userTracker.userHandle
+                    ?.userHandle ?: userTracker.userHandle
             }
             // On work profile devices, SysUI always run in the main user.
             else -> userTracker.userHandle
@@ -158,19 +155,14 @@
      *
      * That will let users open other apps in full screen, and take contextual notes.
      */
-    fun showNoteTask(
-        entryPoint: NoteTaskEntryPoint,
-    ) {
+    fun showNoteTask(entryPoint: NoteTaskEntryPoint) {
         if (!isEnabled) return
 
         showNoteTaskAsUser(entryPoint, getUserForHandlingNotesTaking(entryPoint))
     }
 
     /** A variant of [showNoteTask] which launches note task in the given [user]. */
-    fun showNoteTaskAsUser(
-        entryPoint: NoteTaskEntryPoint,
-        user: UserHandle,
-    ) {
+    fun showNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) {
         if (!isEnabled) return
 
         applicationScope.launch("$TAG#showNoteTaskAsUser") {
@@ -178,10 +170,7 @@
         }
     }
 
-    private suspend fun awaitShowNoteTaskAsUser(
-        entryPoint: NoteTaskEntryPoint,
-        user: UserHandle,
-    ) {
+    private suspend fun awaitShowNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) {
         if (!isEnabled) return
 
         if (!noteTaskBubblesController.areBubblesAvailable()) {
@@ -222,7 +211,13 @@
                     val intent = createNoteTaskIntent(info)
                     val icon =
                         Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
-                    noteTaskBubblesController.showOrHideAppBubble(intent, user, icon)
+                    noteTaskBubblesController.showOrHideAppBubble(
+                        intent,
+                        user,
+                        icon,
+                        info.launchMode.bubbleExpandBehavior,
+                    )
+
                     // App bubble logging happens on `onBubbleExpandChanged`.
                     debugLog { "onShowNoteTask - opened as app bubble: $info" }
                 }
@@ -399,8 +394,8 @@
         const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package"
 
         /** Returns notes role holder settings intent. */
-        fun createNotesRoleHolderSettingsIntent() = Intent(Intent.ACTION_MANAGE_DEFAULT_APP).
-            putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES)
+        fun createNotesRoleHolderSettingsIntent() =
+            Intent(Intent.ACTION_MANAGE_DEFAULT_APP).putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt
index 269eb87..8319e07 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt
@@ -31,6 +31,10 @@
         if (isKeyguardLocked || entryPoint == WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE) {
             NoteTaskLaunchMode.Activity
         } else {
-            NoteTaskLaunchMode.AppBubble
+            if (entryPoint == NoteTaskEntryPoint.QS_NOTES_TILE) {
+                NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED)
+            } else {
+                NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.DEFAULT)
+            }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt
index 836e103f..6c85f20 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt
@@ -26,7 +26,8 @@
 sealed class NoteTaskLaunchMode {
 
     /** @see Bubbles.showOrHideAppBubble */
-    object AppBubble : NoteTaskLaunchMode()
+    data class AppBubble(val bubbleExpandBehavior: NoteTaskBubbleExpandBehavior) :
+        NoteTaskLaunchMode()
 
     /** @see Context.startActivity */
     object Activity : NoteTaskLaunchMode()
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index a1c5c9c..5d54656 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
 import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
@@ -106,12 +107,14 @@
             stateInteractor: NotesTileDataInteractor,
             userActionInteractor: NotesTileUserActionInteractor,
         ): QSTileViewModel =
-            factory.create(
-                TileSpec.create(NOTES_TILE_SPEC),
-                userActionInteractor,
-                stateInteractor,
-                mapper,
-            )
+            if (com.android.systemui.Flags.qsNewTilesFuture())
+                factory.create(
+                    TileSpec.create(NOTES_TILE_SPEC),
+                    userActionInteractor,
+                    stateInteractor,
+                    mapper,
+                )
+            else StubQSTileViewModel
 
         @Provides
         @IntoMap
@@ -120,10 +123,10 @@
             QSTileConfig(
                 tileSpec = TileSpec.create(NOTES_TILE_SPEC),
                 uiConfig =
-                QSTileUIConfig.Resource(
-                    iconRes = R.drawable.ic_qs_notes,
-                    labelRes = R.string.quick_settings_notes_label,
-                ),
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.ic_qs_notes,
+                        labelRes = R.string.quick_settings_notes_label,
+                    ),
                 instanceId = uiEventLogger.getNewInstanceId(),
                 category = TileCategory.UTILITIES,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt
index 776a8f4..c57b53b 100644
--- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt
@@ -26,6 +26,9 @@
     /** The physical power button was pressed to wake up or sleep the device. */
     POWER_BUTTON(isTouch = false, PowerManager.WAKE_REASON_POWER_BUTTON),
 
+    /** The sleep button was pressed to sleep the device. */
+    SLEEP_BUTTON(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON),
+
     /** The user has tapped or double tapped to wake the screen. */
     TAP(isTouch = true, PowerManager.WAKE_REASON_TAP),
 
@@ -78,6 +81,7 @@
         fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason {
             return when (reason) {
                 PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON
+                PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON -> SLEEP_BUTTON
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT
                 PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD
                 else -> OTHER
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 4e094cc..789fdeb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -16,12 +16,17 @@
 
 package com.android.systemui.qs.panels.ui.compose
 
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement.spacedBy
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.shape.CornerSize
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -39,16 +44,17 @@
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.development.ui.compose.BuildNumber
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
-import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
-import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
+import com.android.systemui.qs.panels.ui.compose.Dimensions.FooterHeight
+import com.android.systemui.qs.panels.ui.compose.Dimensions.InterPageSpacing
 import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.ui.compose.borderOnFocus
@@ -121,38 +127,78 @@
                     TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
                 }
             }
-            // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is
-            // expected to be inside a scrollable container, this should not be an issue.
-            Box(modifier = Modifier.requiredHeight(FooterHeight).fillMaxWidth()) {
-                PagerDots(
-                    pagerState = pagerState,
-                    activeColor = MaterialTheme.colorScheme.primary,
-                    nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
-                    modifier = Modifier.align(Alignment.Center),
-                )
-                CompositionLocalProvider(value = LocalContentColor provides Color.White) {
-                    IconButton(
-                        onClick = editModeStart,
-                        shape = RoundedCornerShape(CornerSize(28.dp)),
-                        modifier =
-                            Modifier.align(Alignment.CenterEnd)
-                                .borderOnFocus(
-                                    color = MaterialTheme.colorScheme.secondary,
-                                    cornerSize = CornerSize(FooterHeight / 2),
-                                ),
-                    ) {
-                        Icon(
-                            imageVector = Icons.Default.Edit,
-                            contentDescription = stringResource(id = R.string.qs_edit),
+            FooterBar(
+                buildNumberViewModelFactory = viewModel.buildNumberViewModelFactory,
+                pagerState = pagerState,
+                editModeStart = editModeStart,
+            )
+        }
+    }
+}
+
+private object Dimensions {
+    val FooterHeight = 48.dp
+    val InterPageSpacing = 16.dp
+}
+
+@Composable
+private fun FooterBar(
+    buildNumberViewModelFactory: BuildNumberViewModel.Factory,
+    pagerState: PagerState,
+    editModeStart: () -> Unit,
+) {
+    // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is
+    // expected to be inside a scrollable container, this should not be an issue.
+    // Also, we construct the layout this way to do the following:
+    // * PagerDots is centered in the row, taking as much space as it needs.
+    // * On the start side, we place the BuildNumber, taking as much space as it needs, but
+    //   constrained by the available space left over after PagerDots
+    // * On the end side, we place the edit mode button, with the same constraints as for
+    //   BuildNumber (but it will usually fit, as it's just a square button).
+    Row(
+        modifier = Modifier.requiredHeight(FooterHeight).fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = spacedBy(8.dp),
+    ) {
+        Row(Modifier.weight(1f)) {
+            BuildNumber(
+                viewModelFactory = buildNumberViewModelFactory,
+                textColor = MaterialTheme.colorScheme.onSurface,
+                modifier =
+                    Modifier.borderOnFocus(
+                            color = MaterialTheme.colorScheme.secondary,
+                            cornerSize = CornerSize(1.dp),
                         )
-                    }
+                        .wrapContentSize(),
+            )
+            Spacer(modifier = Modifier.weight(1f))
+        }
+        PagerDots(
+            pagerState = pagerState,
+            activeColor = MaterialTheme.colorScheme.primary,
+            nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
+            modifier = Modifier.wrapContentWidth(),
+        )
+        Row(Modifier.weight(1f)) {
+            Spacer(modifier = Modifier.weight(1f))
+            CompositionLocalProvider(
+                value = LocalContentColor provides MaterialTheme.colorScheme.onSurface
+            ) {
+                IconButton(
+                    onClick = editModeStart,
+                    shape = RoundedCornerShape(CornerSize(28.dp)),
+                    modifier =
+                        Modifier.borderOnFocus(
+                            color = MaterialTheme.colorScheme.secondary,
+                            cornerSize = CornerSize(FooterHeight / 2),
+                        ),
+                ) {
+                    Icon(
+                        imageVector = Icons.Default.Edit,
+                        contentDescription = stringResource(id = R.string.qs_edit),
+                    )
                 }
             }
         }
     }
-
-    private object Dimensions {
-        val FooterHeight = 48.dp
-        val InterPageSpacing = 16.dp
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index cb57c67..0a80a19 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -126,7 +126,7 @@
     val currentBounceableInfo by rememberUpdatedState(bounceableInfo)
     val resources = resources()
     val uiState = remember(state, resources) { state.toUiState(resources) }
-    val colors = TileDefaults.getColorForState(uiState)
+    val colors = TileDefaults.getColorForState(uiState, iconOnly)
     val hapticsViewModel: TileHapticsViewModel? =
         rememberViewModel(traceName = "TileHapticsViewModel") {
             tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile)
@@ -365,22 +365,24 @@
         )
 
     @Composable
-    fun getColorForState(uiState: TileUiState): TileColors {
+    fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors {
         return when (uiState.state) {
             STATE_ACTIVE -> {
-                if (uiState.handlesSecondaryClick) {
+                if (uiState.handlesSecondaryClick && !iconOnly) {
                     activeDualTargetTileColors()
                 } else {
                     activeTileColors()
                 }
             }
+
             STATE_INACTIVE -> {
-                if (uiState.handlesSecondaryClick) {
+                if (uiState.handlesSecondaryClick && !iconOnly) {
                     inactiveDualTargetTileColors()
                 } else {
                     inactiveTileColors()
                 }
             }
+
             else -> unavailableTileColors()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
index e5607eb..bff330b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import androidx.compose.runtime.getValue
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
@@ -34,6 +35,7 @@
     columnsWithMediaViewModelFactory: QSColumnsViewModel.Factory,
     paginatedGridInteractor: PaginatedGridInteractor,
     inFirstPageViewModel: InFirstPageViewModel,
+    val buildNumberViewModelFactory: BuildNumberViewModel.Factory,
 ) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() {
 
     private val hydrator = Hydrator("PaginatedGridViewModel")
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 35b1b96..ab3862b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -81,7 +81,7 @@
                             // additional
                             // guidance on how to auto add your tile
                             throw UnsupportedOperationException(
-                                "Turning on tile is not supported now"
+                                "Turning on tile is not supported now. Tile spec: $tileSpec"
                             )
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 9125d7e..aece5c6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor.Companion.keyguardScenes
 import com.android.systemui.model.SceneContainerPlugin
 import com.android.systemui.model.SysUiState
 import com.android.systemui.model.updateFlags
@@ -352,14 +353,13 @@
                     val isAlternateBouncerVisible = alternateBouncerInteractor.isVisibleState()
                     val isOnPrimaryBouncer = renderedScenes.contains(Scenes.Bouncer)
                     if (!deviceUnlockStatus.isUnlocked) {
-                        return@mapNotNull if (isOnLockscreen || isOnPrimaryBouncer) {
-                            // Already on lockscreen or bouncer, no need to change scenes.
+                        return@mapNotNull if (renderedScenes.any { it in keyguardScenes }) {
+                            // Already on a keyguard scene, no need to change scenes.
                             null
                         } else {
-                            // The device locked while on a scene that's not Lockscreen or Bouncer,
-                            // go to Lockscreen.
-                            Scenes.Lockscreen to
-                                "device locked in non-Lockscreen and non-Bouncer scene"
+                            // The device locked while on a scene that's not a keyguard scene, go
+                            // to Lockscreen.
+                            Scenes.Lockscreen to "device locked in a non-keyguard scene"
                         }
                     }
 
@@ -430,8 +430,13 @@
                                             "mechanism: ${deviceUnlockStatus.deviceUnlockSource}"
                                 else -> null
                             }
-                        // Not on lockscreen or bouncer, so remain in the current scene.
-                        else -> null
+                        // Not on lockscreen or bouncer, so remain in the current scene but since
+                        // unlocked, replace the Lockscreen scene from the bottom of the navigation
+                        // back stack with the Gone scene.
+                        else -> {
+                            replaceLockscreenSceneOnBackStack()
+                            null
+                        }
                     }
                 }
                 .collect { (targetSceneKey, loggingReason) ->
@@ -440,17 +445,19 @@
         }
     }
 
-    /** If the [Scenes.Lockscreen] is on the backstack, replaces it with [Scenes.Gone]. */
+    /**
+     * If the [Scenes.Lockscreen] is on the bottom of the navigation backstack, replaces it with
+     * [Scenes.Gone].
+     */
     private fun replaceLockscreenSceneOnBackStack() {
         sceneBackInteractor.updateBackStack { stack ->
             val list = stack.asIterable().toMutableList()
-            check(list.last() == Scenes.Lockscreen) {
-                "The bottommost/last SceneKey of the back stack isn't" +
-                    " the Lockscreen scene like expected. The back" +
-                    " stack is $stack."
+            if (list.lastOrNull() == Scenes.Lockscreen) {
+                list[list.size - 1] = Scenes.Gone
+                sceneStackOf(*list.toTypedArray())
+            } else {
+                stack
             }
-            list[list.size - 1] = Scenes.Gone
-            sceneStackOf(*list.toTypedArray())
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 4ccd2b9..42a756c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3109,17 +3109,20 @@
         if (isTracking()) {
             onTrackingStopped(true);
         }
-        if (isExpanded() && mBarState != KEYGUARD && !mQsController.getExpanded()) {
-            mShadeLog.d("Status Bar was long pressed. Expanding to QS.");
-            expandToQs();
-        } else {
-            if (mBarState == KEYGUARD) {
-                mShadeLog.d("Lockscreen Status Bar was long pressed. Expanding to Notifications.");
-                mLockscreenShadeTransitionController.goToLockedShade(
-                        /* expandedView= */null, /* needsQSAnimation= */false);
+        if (!mQsController.getExpanded()) {
+            performHapticFeedback(HapticFeedbackConstants.GESTURE_START);
+            if (isExpanded() && mBarState != KEYGUARD) {
+                mShadeLog.d("Status Bar was long pressed. Expanding to QS.");
+                mQsController.flingQs(0, FLING_EXPAND);
             } else {
-                mShadeLog.d("Status Bar was long pressed. Expanding to Notifications.");
-                expandToNotifications();
+                if (mBarState == KEYGUARD) {
+                    mShadeLog.d("Lockscreen Status Bar was long pressed. Expanding to Notifications.");
+                    mLockscreenShadeTransitionController.goToLockedShade(
+                            /* expandedView= */null, /* needsQSAnimation= */true);
+                } else {
+                    mShadeLog.d("Status Bar was long pressed. Expanding to Notifications.");
+                    expandToNotifications();
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 830649b..04f89be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -114,6 +114,7 @@
 import java.io.PrintWriter;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 /** Handles QuickSettings touch handling, expansion and animation state
  * TODO (b/264460656) make this dumpable
@@ -141,6 +142,7 @@
     private final NotificationShadeDepthController mDepthController;
     private final ShadeHeaderController mShadeHeaderController;
     private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    private final Provider<StatusBarLongPressGestureDetector> mStatusBarLongPressGestureDetector;
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardBypassController mKeyguardBypassController;
     private final NotificationRemoteInputManager mRemoteInputManager;
@@ -316,6 +318,7 @@
             NotificationShadeDepthController notificationShadeDepthController,
             ShadeHeaderController shadeHeaderController,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
+            Provider<StatusBarLongPressGestureDetector> statusBarLongPressGestureDetector,
             KeyguardStateController keyguardStateController,
             KeyguardBypassController keyguardBypassController,
             ScrimController scrimController,
@@ -364,6 +367,7 @@
         mDepthController = notificationShadeDepthController;
         mShadeHeaderController = shadeHeaderController;
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
+        mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector;
         mKeyguardStateController = keyguardStateController;
         mKeyguardBypassController = keyguardBypassController;
         mScrimController = scrimController;
@@ -1648,6 +1652,10 @@
         if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
             return false;
         }
+        boolean isInStatusBar = event.getY(event.getActionIndex()) < mStatusBarMinHeight;
+        if (ShadeExpandsOnStatusBarLongPress.isEnabled() && isInStatusBar) {
+            mStatusBarLongPressGestureDetector.get().handleTouch(event);
+        }
         final int action = event.getActionMasked();
         boolean collapsedQs = !getExpanded() && !mSplitShadeEnabled;
         boolean expandedShadeCollapsedQs = mShadeExpandedFraction == 1f
@@ -1684,9 +1692,7 @@
         if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed && isExpansionEnabled()) {
             mTwoFingerExpandPossible = true;
         }
-        if (mTwoFingerExpandPossible && isOpenQsEvent(event)
-                && event.getY(event.getActionIndex())
-                < mStatusBarMinHeight) {
+        if (mTwoFingerExpandPossible && isOpenQsEvent(event) && isInStatusBar) {
             mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1);
             setExpandImmediate(true);
             mNotificationStackScrollLayoutController.setShouldShowShelfOnly(!mSplitShadeEnabled);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index fed4a26..4f73a345 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -164,10 +164,8 @@
 
     @Provides
     @IntoMap
-    @ClassKey(ShadeDisplaysRepositoryImpl::class)
-    fun provideShadePositionRepositoryAsCoreStartable(
-        impl: ShadeDisplaysRepositoryImpl
-    ): CoreStartable {
+    @ClassKey(ShadePrimaryDisplayCommand::class)
+    fun provideShadePrimaryDisplayCommand(impl: ShadePrimaryDisplayCommand): CoreStartable {
         return if (ShadeWindowGoesAround.isEnabled) {
             impl
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
index 506b4e9..a5d9e96 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -17,40 +17,107 @@
 package com.android.systemui.shade
 
 import android.view.Display
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.display.data.repository.DisplayRepository
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
 import java.io.PrintWriter
+import javax.inject.Inject
 
-class ShadePrimaryDisplayCommand(private val positionRepository: ShadeDisplaysRepository) :
-    Command {
+@SysUISingleton
+class ShadePrimaryDisplayCommand
+@Inject
+constructor(
+    private val commandRegistry: CommandRegistry,
+    private val displaysRepository: DisplayRepository,
+    private val positionRepository: ShadeDisplaysRepository,
+) : Command, CoreStartable {
 
-    override fun execute(pw: PrintWriter, args: List<String>) {
-        if (args[0].lowercase() == "reset") {
-            positionRepository.resetDisplayId()
-            pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}")
-            return
-        }
-
-        val displayId: Int =
-            try {
-                args[0].toInt()
-            } catch (e: NumberFormatException) {
-                pw.println("Error: task id should be an integer")
-                return
-            }
-
-        if (displayId < 0) {
-            pw.println("Error: display id should be positive integer")
-        }
-
-        positionRepository.setDisplayId(displayId)
-        pw.println("New shade primary display id is $displayId")
+    override fun start() {
+        commandRegistry.registerCommand("shade_display_override") { this }
     }
 
     override fun help(pw: PrintWriter) {
         pw.println("shade_display_override <displayId> ")
         pw.println("Set the display which is holding the shade.")
+        pw.println()
         pw.println("shade_display_override reset ")
         pw.println("Reset the display which is holding the shade.")
+        pw.println()
+        pw.println("shade_display_override (list|status) ")
+        pw.println("Lists available displays and which has the shade")
+        pw.println()
+        pw.println("shade_display_override any_external")
+        pw.println("Moves the shade to the first not-default display available")
+    }
+
+    override fun execute(pw: PrintWriter, args: List<String>) {
+        CommandHandler(pw, args).execute()
+    }
+
+    /** Wrapper class to avoid propagating [PrintWriter] to all methods. */
+    private inner class CommandHandler(
+        private val pw: PrintWriter,
+        private val args: List<String>,
+    ) {
+
+        fun execute() {
+            when (val command = args.getOrNull(0)?.lowercase()) {
+                "reset" -> reset()
+                "list",
+                "status" -> printStatus()
+                "any_external" -> anyExternal()
+                else -> {
+                    val cmdAsInteger = command?.toIntOrNull()
+                    if (cmdAsInteger != null) {
+                        changeDisplay(displayId = cmdAsInteger)
+                    } else {
+                        help(pw)
+                    }
+                }
+            }
+        }
+
+        private fun reset() {
+            positionRepository.resetDisplayId()
+            pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}")
+        }
+
+        private fun printStatus() {
+            val displays = displaysRepository.displays.value
+            val shadeDisplay = positionRepository.displayId.value
+            pw.println("Available displays: ")
+            displays.forEach {
+                pw.print(" - ${it.displayId}")
+                pw.println(if (it.displayId == shadeDisplay) " (Shade window is here)" else "")
+            }
+        }
+
+        private fun anyExternal() {
+            val anyExternalDisplay =
+                displaysRepository.displays.value.firstOrNull {
+                    it.displayId != Display.DEFAULT_DISPLAY
+                }
+            if (anyExternalDisplay == null) {
+                pw.println("No external displays available.")
+                return
+            }
+            setDisplay(anyExternalDisplay.displayId)
+        }
+
+        private fun changeDisplay(displayId: Int) {
+            if (displayId < 0) {
+                pw.println("Error: display id should be positive integer")
+            }
+
+            setDisplay(displayId)
+        }
+
+        private fun setDisplay(id: Int) {
+            positionRepository.setDisplayId(id)
+            pw.println("New shade primary display id is $id")
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index e920aba..4a95e33 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -17,10 +17,7 @@
 package com.android.systemui.shade.data.repository
 
 import android.view.Display
-import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadePrimaryDisplayCommand
-import com.android.systemui.statusbar.commandline.CommandRegistry
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -41,9 +38,7 @@
 
 /** Source of truth for the display currently holding the shade. */
 @SysUISingleton
-class ShadeDisplaysRepositoryImpl
-@Inject
-constructor(private val commandRegistry: CommandRegistry) : ShadeDisplaysRepository, CoreStartable {
+class ShadeDisplaysRepositoryImpl @Inject constructor() : ShadeDisplaysRepository {
     private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
 
     override val displayId: StateFlow<Int>
@@ -56,10 +51,4 @@
     override fun resetDisplayId() {
         _displayId.value = Display.DEFAULT_DISPLAY
     }
-
-    override fun start() {
-        commandRegistry.registerCommand("shade_display_override") {
-            ShadePrimaryDisplayCommand(this)
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index d1de6be..bda9e06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -19,6 +19,7 @@
 import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 
+import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
 import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE;
@@ -75,8 +76,8 @@
 import com.android.internal.util.ContrastColorUtil;
 import com.android.internal.widget.CachingIconView;
 import com.android.internal.widget.CallLayout;
+import com.android.systemui.Flags;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.PluginListener;
@@ -293,7 +294,7 @@
 
     private static boolean shouldSimulateSlowMeasure() {
         return Compile.IS_DEBUG && RefactorFlag.forView(
-                Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE).isEnabled();
+                ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE).isEnabled();
     }
 
     private static final String SLOW_MEASURE_SIMULATE_DELAY_PROPERTY =
@@ -1679,7 +1680,13 @@
         dismiss(fromAccessibility);
         if (canEntryBeDismissed()) {
             if (mOnUserInteractionCallback != null) {
-                mOnUserInteractionCallback.registerFutureDismissal(mEntry, REASON_CANCEL).run();
+                if (Flags.notificationReentrantDismiss()) {
+                    Runnable futureDismissal = mOnUserInteractionCallback.registerFutureDismissal(
+                            mEntry, REASON_CANCEL);
+                    post(futureDismissal);
+                } else {
+                    mOnUserInteractionCallback.registerFutureDismissal(mEntry, REASON_CANCEL).run();
+                }
             }
         }
     }
@@ -1986,7 +1993,7 @@
         mColorUpdateLogger = colorUpdateLogger;
         mDismissibilityProvider = dismissibilityProvider;
         mFeatureFlags = featureFlags;
-        setHapticFeedbackEnabled(!com.android.systemui.Flags.msdlFeedback());
+        setHapticFeedbackEnabled(!Flags.msdlFeedback());
     }
 
     private void initDimens() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 2dcb706..d0c033b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -834,6 +834,15 @@
             public?.let {
                 it.layoutInflaterFactory = provider.provide(row, FLAG_CONTENT_VIEW_PUBLIC)
             }
+            if (android.app.Flags.notificationsRedesignAppIcons()) {
+                normalGroupHeader?.let {
+                    it.layoutInflaterFactory = provider.provide(row, FLAG_GROUP_SUMMARY_HEADER)
+                }
+                minimizedGroupHeader?.let {
+                    it.layoutInflaterFactory =
+                        provider.provide(row, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)
+                }
+            }
             return this
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 80c8e8b..db29493 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -171,14 +171,12 @@
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeLogger;
 import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.ShadeViewController;
-import com.android.systemui.shade.StatusBarLongPressGestureDetector;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.AutoHideUiElement;
@@ -368,7 +366,6 @@
 
     private PhoneStatusBarViewController mPhoneStatusBarViewController;
     private PhoneStatusBarTransitions mStatusBarTransitions;
-    private final Provider<StatusBarLongPressGestureDetector> mStatusBarLongPressGestureDetector;
     private final AuthRippleController mAuthRippleController;
     @WindowVisibleState private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
@@ -674,7 +671,6 @@
             ShadeController shadeController,
             WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            Provider<StatusBarLongPressGestureDetector> statusBarLongPressGestureDetector,
             ViewMediatorCallback viewMediatorCallback,
             InitController initController,
             @Named(TIME_TICK_HANDLER_NAME) Handler timeTickHandler,
@@ -782,7 +778,6 @@
         mShadeController = shadeController;
         mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-        mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector;
         mKeyguardViewMediatorCallback = viewMediatorCallback;
         mInitController = initController;
         mPluginDependencyProvider = pluginDependencyProvider;
@@ -1532,11 +1527,6 @@
                 // to touch outside the customizer to close it, such as on the status or nav bar.
                 mShadeController.onStatusBarTouch(event);
             }
-            if (ShadeExpandsOnStatusBarLongPress.isEnabled()
-                    && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
-                mStatusBarLongPressGestureDetector.get().handleTouch(event);
-            }
-
             return getNotificationShadeWindowView().onTouchEvent(event);
         };
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 7ef1e41..5837a49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -31,6 +31,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -63,17 +64,21 @@
 
     @Override
     public void addCallback(@NonNull Callback callback) {
-        mCallbacks.add(callback);
-        if (mCallbacks.size() == 1) {
-            setListening(true);
+        synchronized (mCallbacks) {
+            mCallbacks.add(callback);
+            if (mCallbacks.size() == 1) {
+                setListening(true);
+            }
+            callback.onManagedProfileChanged();
         }
-        callback.onManagedProfileChanged();
     }
 
     @Override
     public void removeCallback(@NonNull Callback callback) {
-        if (mCallbacks.remove(callback) && mCallbacks.size() == 0) {
-            setListening(false);
+        synchronized (mCallbacks) {
+            if (mCallbacks.remove(callback) && mCallbacks.size() == 0) {
+                setListening(false);
+            }
         }
     }
 
@@ -109,10 +114,7 @@
     }
 
     private void notifyManagedProfileRemoved() {
-        ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
-        for (Callback callback : copy) {
-            callback.onManagedProfileRemoved();
-        }
+        notifyCallbacks(Callback::onManagedProfileRemoved);
     }
 
     public boolean hasActiveProfile() {
@@ -136,6 +138,16 @@
         }
     }
 
+    private void notifyCallbacks(Consumer<Callback> method) {
+        ArrayList<Callback> copy;
+        synchronized (mCallbacks) {
+            copy = new ArrayList<>(mCallbacks);
+        }
+        for (Callback callback : copy) {
+            method.accept(callback);
+        }
+    }
+
     private void setListening(boolean listening) {
         if (mListening == listening) {
             return;
@@ -154,19 +166,13 @@
         @Override
         public void onUserChanged(int newUser, @NonNull Context userContext) {
             reloadManagedProfiles();
-            ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
-            for (Callback callback : copy) {
-                callback.onManagedProfileChanged();
-            }
+            notifyCallbacks(Callback::onManagedProfileChanged);
         }
 
         @Override
         public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
             reloadManagedProfiles();
-            ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
-            for (Callback callback : copy) {
-                callback.onManagedProfileChanged();
-            }
+            notifyCallbacks(Callback::onManagedProfileChanged);
         }
     }
 }
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 f6f567f..298ef7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -88,7 +88,7 @@
         implements HeadsUpManager, HeadsUpRepository, OnHeadsUpChangedListener {
     private static final String TAG = "BaseHeadsUpManager";
     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
-
+    private static final String REASON_REORDER_ALLOWED = "mOnReorderingAllowedListener";
     protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
 
     protected final Context mContext;
@@ -633,7 +633,7 @@
             }
             entry.demoteStickyHun();
             mHeadsUpEntryMap.remove(key);
-            onEntryRemoved(finalHeadsUpEntry);
+            onEntryRemoved(finalHeadsUpEntry, reason);
             // TODO(b/328390331) move accessibility events to the view layer
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
             if (NotificationThrottleHun.isEnabled()) {
@@ -648,8 +648,9 @@
     /**
      * Manager-specific logic that should occur when an entry is removed.
      * @param headsUpEntry entry removed
+     * @param reason why onEntryRemoved was called
      */
-    protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
+    protected void onEntryRemoved(HeadsUpEntry headsUpEntry, String reason) {
         NotificationEntry entry = headsUpEntry.mEntry;
         entry.setHeadsUp(false);
         setEntryPinned(headsUpEntry, false /* isPinned */, "onEntryRemoved");
@@ -664,10 +665,17 @@
         updateTopHeadsUpFlow();
         updateHeadsUpFlow();
         if (NotificationThrottleHun.isEnabled()) {
-            if (headsUpEntry.mEntry != null) {
-                if (mEntriesToRemoveWhenReorderingAllowed.contains(headsUpEntry.mEntry)) {
-                    mEntriesToRemoveWhenReorderingAllowed.remove(headsUpEntry.mEntry);
-                }
+            NotificationEntry notifEntry = headsUpEntry.mEntry;
+            if (notifEntry == null) {
+                return;
+            }
+            // If reorder was just allowed and we called onEntryRemoved while iterating over
+            // mEntriesToRemoveWhenReorderingAllowed, we should not remove from this list (and cause
+            // ArrayIndexOutOfBoundsException). We don't need to in this case anyway, because we
+            // clear mEntriesToRemoveWhenReorderingAllowed after removing these entries.
+            if (!reason.equals(REASON_REORDER_ALLOWED)
+                    && mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)) {
+                mEntriesToRemoveWhenReorderingAllowed.remove(notifEntry);
             }
         }
     }
@@ -1135,7 +1143,8 @@
                 && Notification.CATEGORY_CALL.equals(n.category));
     }
 
-    private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
+    @VisibleForTesting
+    public final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
         if (NotificationThrottleHun.isEnabled()) {
             mAvalancheController.setEnableAtRuntime(true);
             if (mEntriesToRemoveWhenReorderingAllowed.isEmpty()) {
@@ -1146,7 +1155,7 @@
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (entry != null && isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
-                removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
+                removeEntry(entry.getKey(), REASON_REORDER_ALLOWED);
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index d371acf..66a900b 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.Configuration
 import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -36,8 +37,12 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.input.pointer.pointerInteropFilter
@@ -49,6 +54,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.touchpad.tutorial.ui.gesture.isFourFingerTouchpadSwipe
 import com.android.systemui.touchpad.tutorial.ui.gesture.isThreeFingerTouchpadSwipe
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen
 
 @Composable
 fun TutorialSelectionScreen(
@@ -56,6 +62,7 @@
     onHomeTutorialClicked: () -> Unit,
     onRecentAppsTutorialClicked: () -> Unit,
     onDoneButtonClicked: () -> Unit,
+    lastSelectedScreen: Screen,
 ) {
     Column(
         verticalArrangement = Arrangement.Center,
@@ -80,6 +87,7 @@
                     onHomeTutorialClicked = onHomeTutorialClicked,
                     onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
                     modifier = Modifier.weight(1f).padding(60.dp),
+                    lastSelectedScreen,
                 )
             }
             else -> {
@@ -88,6 +96,7 @@
                     onHomeTutorialClicked = onHomeTutorialClicked,
                     onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
                     modifier = Modifier.weight(1f).padding(60.dp),
+                    lastSelectedScreen,
                 )
             }
         }
@@ -105,6 +114,7 @@
     onHomeTutorialClicked: () -> Unit,
     onRecentAppsTutorialClicked: () -> Unit,
     modifier: Modifier = Modifier,
+    lastSelectedScreen: Screen,
 ) {
     Row(
         horizontalArrangement = Arrangement.spacedBy(20.dp),
@@ -116,6 +126,7 @@
             onHomeTutorialClicked,
             onRecentAppsTutorialClicked,
             modifier = Modifier.weight(1f).fillMaxSize(),
+            lastSelectedScreen,
         )
     }
 }
@@ -126,6 +137,7 @@
     onHomeTutorialClicked: () -> Unit,
     onRecentAppsTutorialClicked: () -> Unit,
     modifier: Modifier = Modifier,
+    lastSelectedScreen: Screen,
 ) {
     Column(
         verticalArrangement = Arrangement.spacedBy(20.dp),
@@ -137,6 +149,7 @@
             onHomeTutorialClicked,
             onRecentAppsTutorialClicked,
             modifier = Modifier.weight(1f).fillMaxSize(),
+            lastSelectedScreen,
         )
     }
 }
@@ -147,14 +160,26 @@
     onHomeTutorialClicked: () -> Unit,
     onRecentAppsTutorialClicked: () -> Unit,
     modifier: Modifier = Modifier,
+    lastSelectedScreen: Screen,
 ) {
+    val homeFocusRequester = remember { FocusRequester() }
+    val backFocusRequester = remember { FocusRequester() }
+    val recentAppsFocusRequester = remember { FocusRequester() }
+    LaunchedEffect(Unit) {
+        when (lastSelectedScreen) {
+            Screen.HOME_GESTURE -> homeFocusRequester.requestFocus()
+            Screen.BACK_GESTURE -> backFocusRequester.requestFocus()
+            Screen.RECENT_APPS_GESTURE -> recentAppsFocusRequester.requestFocus()
+            else -> {} // No-Op.
+        }
+    }
     TutorialButton(
         text = stringResource(R.string.touchpad_tutorial_home_gesture_button),
         icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
         iconColor = MaterialTheme.colorScheme.onPrimary,
         onClick = onHomeTutorialClicked,
         backgroundColor = MaterialTheme.colorScheme.primary,
-        modifier = modifier,
+        modifier = modifier.focusRequester(homeFocusRequester).focusable(),
     )
     TutorialButton(
         text = stringResource(R.string.touchpad_tutorial_back_gesture_button),
@@ -162,7 +187,7 @@
         iconColor = MaterialTheme.colorScheme.onTertiary,
         onClick = onBackTutorialClicked,
         backgroundColor = MaterialTheme.colorScheme.tertiary,
-        modifier = modifier,
+        modifier = modifier.focusRequester(backFocusRequester).focusable(),
     )
     TutorialButton(
         text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button),
@@ -170,7 +195,7 @@
         iconColor = MaterialTheme.colorScheme.onSecondary,
         onClick = onRecentAppsTutorialClicked,
         backgroundColor = MaterialTheme.colorScheme.secondary,
-        modifier = modifier,
+        modifier = modifier.focusRequester(recentAppsFocusRequester).focusable(),
     )
 }
 
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 e1f7bd5..6662fc5 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
@@ -24,12 +24,16 @@
 import androidx.activity.viewModels
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.lifecycle.Lifecycle.State.STARTED
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 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.res.R
 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
@@ -54,6 +58,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         enableEdgeToEdge()
+        setTitle(getString(R.string.launch_touchpad_tutorial_notification_content))
         setContent {
             PlatformTheme { TouchpadTutorialScreen(vm, closeTutorial = ::finishTutorial) }
         }
@@ -82,13 +87,24 @@
 @Composable
 fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> Unit) {
     val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED)
+    var lastSelectedScreen by remember { mutableStateOf(TUTORIAL_SELECTION) }
     when (activeScreen) {
         TUTORIAL_SELECTION ->
             TutorialSelectionScreen(
-                onBackTutorialClicked = { vm.goTo(BACK_GESTURE) },
-                onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) },
-                onRecentAppsTutorialClicked = { vm.goTo(RECENT_APPS_GESTURE) },
+                onBackTutorialClicked = {
+                    lastSelectedScreen = BACK_GESTURE
+                    vm.goTo(BACK_GESTURE)
+                },
+                onHomeTutorialClicked = {
+                    lastSelectedScreen = HOME_GESTURE
+                    vm.goTo(HOME_GESTURE)
+                },
+                onRecentAppsTutorialClicked = {
+                    lastSelectedScreen = RECENT_APPS_GESTURE
+                    vm.goTo(RECENT_APPS_GESTURE)
+                },
                 onDoneButtonClicked = closeTutorial,
+                lastSelectedScreen,
             )
         BACK_GESTURE ->
             BackGestureTutorialScreen(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index c4b028d..1963ba2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -18,14 +18,18 @@
 
 import android.view.LayoutInflater
 import android.view.View
-import android.view.ViewGroup
 import android.widget.ImageButton
 import androidx.annotation.LayoutRes
 import androidx.compose.ui.util.fastForEachIndexed
+import androidx.constraintlayout.motion.widget.MotionLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.internal.R as internalR
+import com.android.settingslib.Utils
 import com.android.systemui.lifecycle.WindowLifecycleState
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.lifecycle.viewModel
 import com.android.systemui.res.R
+import com.android.systemui.util.children
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState
@@ -33,7 +37,6 @@
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModelState
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel
 import javax.inject.Inject
-import kotlin.math.abs
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 
@@ -44,12 +47,8 @@
 
     fun bind(view: View) {
         with(view) {
-            val drawerAndRingerContainer =
-                requireViewById<View>(R.id.volume_ringer_and_drawer_container)
-            val drawerContainer = requireViewById<View>(R.id.volume_drawer_container)
-            val selectedButtonView =
-                requireViewById<ImageButton>(R.id.volume_new_ringer_active_button)
             val volumeDialogBackgroundView = requireViewById<View>(R.id.volume_dialog_background)
+            val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
             repeatWhenAttached {
                 viewModel(
                     traceName = "VolumeDialogRingerViewBinder",
@@ -62,29 +61,26 @@
                                 is RingerViewModelState.Available -> {
                                     val uiModel = ringerState.uiModel
 
-                                    bindSelectedButton(viewModel, uiModel, selectedButtonView)
-                                    bindDrawerButtons(viewModel, uiModel.availableButtons)
+                                    bindDrawerButtons(viewModel, uiModel)
 
-                                    // Set up views background and visibility
-                                    drawerAndRingerContainer.visibility = View.VISIBLE
+                                    // Set up view background and visibility
+                                    drawerContainer.visibility = View.VISIBLE
                                     when (uiModel.drawerState) {
                                         is RingerDrawerState.Initial -> {
-                                            drawerContainer.visibility = View.GONE
-                                            selectedButtonView.visibility = View.VISIBLE
+                                            drawerContainer.closeDrawer(uiModel.currentButtonIndex)
                                             volumeDialogBackgroundView.setBackgroundResource(
                                                 R.drawable.volume_dialog_background
                                             )
                                         }
                                         is RingerDrawerState.Closed -> {
-                                            drawerContainer.visibility = View.GONE
-                                            selectedButtonView.visibility = View.VISIBLE
+                                            drawerContainer.closeDrawer(uiModel.currentButtonIndex)
                                             volumeDialogBackgroundView.setBackgroundResource(
                                                 R.drawable.volume_dialog_background
                                             )
                                         }
                                         is RingerDrawerState.Open -> {
-                                            drawerContainer.visibility = View.VISIBLE
-                                            selectedButtonView.visibility = View.GONE
+                                            // Open drawer
+                                            drawerContainer.transitionToEnd()
                                             if (
                                                 uiModel.currentButtonIndex !=
                                                     uiModel.availableButtons.size - 1
@@ -97,7 +93,7 @@
                                     }
                                 }
                                 is RingerViewModelState.Unavailable -> {
-                                    drawerAndRingerContainer.visibility = View.GONE
+                                    drawerContainer.visibility = View.GONE
                                     volumeDialogBackgroundView.setBackgroundResource(
                                         R.drawable.volume_dialog_background
                                     )
@@ -112,15 +108,21 @@
 
     private fun View.bindDrawerButtons(
         viewModel: VolumeDialogRingerDrawerViewModel,
-        availableButtons: List<RingerButtonViewModel?>,
+        uiModel: RingerViewModel,
     ) {
-        val drawerOptions = requireViewById<ViewGroup>(R.id.volume_drawer_options)
-        val count = availableButtons.size
-        drawerOptions.ensureChildCount(R.layout.volume_ringer_button, count)
+        val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
+        val count = uiModel.availableButtons.size
+        drawerContainer.ensureChildCount(R.layout.volume_ringer_button, count)
 
-        availableButtons.fastForEachIndexed { index, ringerButton ->
+        uiModel.availableButtons.fastForEachIndexed { index, ringerButton ->
             ringerButton?.let {
-                drawerOptions.getChildAt(count - index - 1).bindDrawerButton(it, viewModel)
+                val view = drawerContainer.getChildAt(count - index - 1)
+                // TODO (b/369995871): object animator for button switch ( active <-> inactive )
+                if (index == uiModel.currentButtonIndex) {
+                    view.bindDrawerButton(uiModel.selectedButton, viewModel, isSelected = true)
+                } else {
+                    view.bindDrawerButton(it, viewModel)
+                }
             }
         }
     }
@@ -128,15 +130,29 @@
     private fun View.bindDrawerButton(
         buttonViewModel: RingerButtonViewModel,
         viewModel: VolumeDialogRingerDrawerViewModel,
+        isSelected: Boolean = false,
     ) {
         with(requireViewById<ImageButton>(R.id.volume_drawer_button)) {
             setImageResource(buttonViewModel.imageResId)
             contentDescription = context.getString(buttonViewModel.contentDescriptionResId)
-            setOnClickListener { viewModel.onRingerButtonClicked(buttonViewModel.ringerMode) }
+            if (isSelected) {
+                setBackgroundResource(R.drawable.volume_drawer_selection_bg)
+                setColorFilter(
+                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnPrimary)
+                )
+            } else {
+                setBackgroundResource(R.drawable.volume_ringer_item_bg)
+                setColorFilter(
+                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnSurface)
+                )
+            }
+            setOnClickListener {
+                viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
+            }
         }
     }
 
-    private fun ViewGroup.ensureChildCount(@LayoutRes viewLayoutId: Int, count: Int) {
+    private fun MotionLayout.ensureChildCount(@LayoutRes viewLayoutId: Int, count: Int) {
         val childCountDelta = childCount - count
         when {
             childCountDelta > 0 -> {
@@ -144,21 +160,107 @@
             }
             childCountDelta < 0 -> {
                 val inflater = LayoutInflater.from(context)
-                repeat(abs(childCountDelta)) { inflater.inflate(viewLayoutId, this, true) }
+                repeat(-childCountDelta) {
+                    inflater.inflate(viewLayoutId, this, true)
+                    getChildAt(childCount - 1).id = View.generateViewId()
+                }
+                cloneConstraintSet(R.id.volume_dialog_ringer_drawer_open)
+                    .adjustOpenConstraintsForDrawer(this)
             }
         }
     }
 
-    private fun bindSelectedButton(
-        viewModel: VolumeDialogRingerDrawerViewModel,
-        uiModel: RingerViewModel,
-        selectedButtonView: ImageButton,
-    ) {
-        with(uiModel) {
-            selectedButtonView.setImageResource(selectedButton.imageResId)
-            selectedButtonView.setOnClickListener {
-                viewModel.onRingerButtonClicked(selectedButton.ringerMode)
+    private fun MotionLayout.closeDrawer(selectedIndex: Int) {
+        cloneConstraintSet(R.id.volume_dialog_ringer_drawer_close)
+            .adjustClosedConstraintsForDrawer(selectedIndex, this)
+        transitionToStart()
+    }
+
+    private fun ConstraintSet.adjustOpenConstraintsForDrawer(motionLayout: MotionLayout) {
+        motionLayout.children.forEachIndexed { index, button ->
+            setButtonPositionConstraints(motionLayout, index, button)
+            setAlpha(button.id, 1.0F)
+            constrainWidth(
+                button.id,
+                motionLayout.context.resources.getDimensionPixelSize(
+                    R.dimen.volume_dialog_ringer_drawer_button_size
+                ),
+            )
+            constrainHeight(
+                button.id,
+                motionLayout.context.resources.getDimensionPixelSize(
+                    R.dimen.volume_dialog_ringer_drawer_button_size
+                ),
+            )
+            if (index != motionLayout.childCount - 1) {
+                setMargin(
+                    button.id,
+                    ConstraintSet.BOTTOM,
+                    motionLayout.context.resources.getDimensionPixelSize(
+                        R.dimen.volume_dialog_components_spacing
+                    ),
+                )
             }
         }
+        motionLayout.updateState(R.id.volume_dialog_ringer_drawer_open, this)
+    }
+
+    private fun ConstraintSet.adjustClosedConstraintsForDrawer(
+        selectedIndex: Int,
+        motionLayout: MotionLayout,
+    ) {
+        motionLayout.children.forEachIndexed { index, button ->
+            setButtonPositionConstraints(motionLayout, index, button)
+            constrainWidth(
+                button.id,
+                motionLayout.context.resources.getDimensionPixelSize(
+                    R.dimen.volume_dialog_ringer_drawer_button_size
+                ),
+            )
+            if (selectedIndex != motionLayout.childCount - index - 1) {
+                setAlpha(button.id, 0.0F)
+                constrainHeight(button.id, 0)
+                setMargin(button.id, ConstraintSet.BOTTOM, 0)
+            } else {
+                setAlpha(button.id, 1.0F)
+                constrainHeight(
+                    button.id,
+                    motionLayout.context.resources.getDimensionPixelSize(
+                        R.dimen.volume_dialog_ringer_drawer_button_size
+                    ),
+                )
+            }
+        }
+        motionLayout.updateState(R.id.volume_dialog_ringer_drawer_close, this)
+    }
+
+    private fun ConstraintSet.setButtonPositionConstraints(
+        motionLayout: MotionLayout,
+        index: Int,
+        button: View,
+    ) {
+        if (motionLayout.getChildAt(index - 1) == null) {
+            connect(button.id, ConstraintSet.TOP, motionLayout.id, ConstraintSet.TOP)
+        } else {
+            connect(
+                button.id,
+                ConstraintSet.TOP,
+                motionLayout.getChildAt(index - 1).id,
+                ConstraintSet.BOTTOM,
+            )
+        }
+
+        if (motionLayout.getChildAt(index + 1) == null) {
+            connect(button.id, ConstraintSet.BOTTOM, motionLayout.id, ConstraintSet.BOTTOM)
+        } else {
+            connect(
+                button.id,
+                ConstraintSet.BOTTOM,
+                motionLayout.getChildAt(index + 1).id,
+                ConstraintSet.TOP,
+            )
+        }
+        connect(button.id, ConstraintSet.START, motionLayout.id, ConstraintSet.START)
+        connect(button.id, ConstraintSet.END, motionLayout.id, ConstraintSet.END)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index e040638..624dcc7 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -84,8 +84,8 @@
             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
             .build()
 
-    fun onRingerButtonClicked(ringerMode: RingerMode) {
-        if (drawerState.value is RingerDrawerState.Open) {
+    fun onRingerButtonClicked(ringerMode: RingerMode, isSelectedButton: Boolean = false) {
+        if (drawerState.value is RingerDrawerState.Open && !isSelectedButton) {
             Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
             provideTouchFeedback(ringerMode)
             maybeShowToast(ringerMode)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
index 538ee47..772ae77 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.volume.dialog.sliders.dagger
 
 import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderTouchesViewBinder
 import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
 import dagger.BindsInstance
 import dagger.Subcomponent
@@ -31,6 +32,8 @@
 
     fun sliderViewBinder(): VolumeDialogSliderViewBinder
 
+    fun sliderTouchesViewBinder(): VolumeDialogSliderTouchesViewBinder
+
     @Subcomponent.Factory
     interface Factory {
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt
new file mode 100644
index 0000000..adc2383
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.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.systemui.volume.dialog.sliders.data.repository
+
+import android.annotation.SuppressLint
+import android.view.MotionEvent
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+@VolumeDialogSliderScope
+class VolumeDialogSliderTouchEventsRepository @Inject constructor() {
+
+    @SuppressLint("SharedFlowCreation")
+    private val mutableSliderTouchEvents: MutableStateFlow<MotionEvent?> = MutableStateFlow(null)
+    val sliderTouchEvent: Flow<MotionEvent> = mutableSliderTouchEvents.filterNotNull()
+
+    fun update(event: MotionEvent) {
+        mutableSliderTouchEvents.tryEmit(MotionEvent.obtain(event))
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt
new file mode 100644
index 0000000..c7b4184
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt
@@ -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.systemui.volume.dialog.sliders.domain.interactor
+
+import android.view.MotionEvent
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogCallbacksInteractor
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.data.repository.VolumeDialogSliderTouchEventsRepository
+import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+
+@VolumeDialogSliderScope
+class VolumeDialogSliderInputEventsInteractor
+@Inject
+constructor(
+    @VolumeDialog coroutineScope: CoroutineScope,
+    volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
+    private val visibilityInteractor: VolumeDialogVisibilityInteractor,
+    private val repository: VolumeDialogSliderTouchEventsRepository,
+) {
+
+    val event: Flow<SliderInputEvent> =
+        merge(
+            repository.sliderTouchEvent.map { SliderInputEvent.Touch(it) },
+            volumeDialogCallbacksInteractor.event
+                .filterIsInstance(VolumeDialogEventModel.VolumeChangedFromKey::class)
+                .map { SliderInputEvent.Button },
+        )
+
+    init {
+        event.onEach { visibilityInteractor.resetDismissTimeout() }.launchIn(coroutineScope)
+    }
+
+    fun onTouchEvent(newEvent: MotionEvent) {
+        repository.update(newEvent)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
index 66a598b..c904ac5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
@@ -35,6 +35,7 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.runningReduce
 import kotlinx.coroutines.flow.stateIn
 
 private const val DEFAULT_STREAM = AudioManager.STREAM_MUSIC
@@ -54,13 +55,17 @@
         volumeDialogStateInteractor.volumeDialogState
             .filter { it.streamModels.isNotEmpty() }
             .map { stateModel ->
-                stateModel.streamModels.values
-                    .filter { streamModel -> shouldShowSliders(stateModel, streamModel) }
-                    .sortedWith(streamsSorter)
+                val sliderTypes =
+                    stateModel.streamModels.values
+                        .filter { streamModel -> shouldShowSliders(stateModel, streamModel) }
+                        .sortedWith(streamsSorter)
+                        .map { model -> model.toType() }
+                LinkedHashSet(sliderTypes)
             }
-            .map { models ->
-                val sliderTypes: List<VolumeDialogSliderType> =
-                    models.map { model -> model.toType() }
+            .runningReduce { sliderTypes, newSliderTypes ->
+                newSliderTypes.apply { addAll(sliderTypes) }
+            }
+            .map { sliderTypes ->
                 VolumeDialogSlidersModel(
                     slider = sliderTypes.first(),
                     floatingSliders = sliderTypes.drop(1),
diff --git a/services/core/java/com/android/server/security/forensic/DataSource.java b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
similarity index 67%
copy from services/core/java/com/android/server/security/forensic/DataSource.java
copy to packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
index da7ee21..37dbb4b 100644
--- a/services/core/java/com/android/server/security/forensic/DataSource.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
@@ -14,16 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.security.forensic;
+package com.android.systemui.volume.dialog.sliders.shared.model
 
-public interface DataSource {
-    /**
-     * Enable the data collection.
-     */
-    void enable();
+import android.view.MotionEvent
 
-    /**
-     * Disable the data collection.
-     */
-    void disable();
+/** Models input event happened on the Volume Slider */
+sealed interface SliderInputEvent {
+
+    data class Touch(val event: MotionEvent) : SliderInputEvent
+
+    data object Button : SliderInputEvent
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt
new file mode 100644
index 0000000..7fd177d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.volume.dialog.sliders.ui
+
+import android.annotation.SuppressLint
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.res.R
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderTouchesViewModel
+import com.google.android.material.slider.Slider
+import javax.inject.Inject
+
+@VolumeDialogSliderScope
+class VolumeDialogSliderTouchesViewBinder
+@Inject
+constructor(private val viewModelFactory: VolumeDialogSliderTouchesViewModel.Factory) {
+
+    @SuppressLint("ClickableViewAccessibility")
+    fun bind(view: View) {
+        with(view.requireViewById<Slider>(R.id.volume_dialog_slider)) {
+            repeatWhenAttached {
+                viewModel(
+                    traceName = "VolumeDialogSliderTouchesViewBinder",
+                    minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+                    factory = { viewModelFactory.create() },
+                ) { viewModel ->
+                    setOnTouchListener { _, event ->
+                        viewModel.onTouchEvent(event)
+                        false
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
index 9078f82..1b2b4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.lifecycle.viewModel
 import com.android.systemui.res.R
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.launchIn
@@ -50,7 +51,7 @@
                 ) { viewModel ->
                     viewModel.sliders
                         .onEach { uiModel ->
-                            uiModel.sliderComponent.sliderViewBinder().bind(mainSliderContainer)
+                            uiModel.sliderComponent.bindSlider(mainSliderContainer)
 
                             val floatingSliderViewBinders = uiModel.floatingSliderComponent
                             floatingSlidersContainer.ensureChildCount(
@@ -58,9 +59,9 @@
                                 count = floatingSliderViewBinders.size,
                             )
                             floatingSliderViewBinders.fastForEachIndexed { index, sliderComponent ->
-                                sliderComponent
-                                    .sliderViewBinder()
-                                    .bind(floatingSlidersContainer.getChildAt(index))
+                                sliderComponent.bindSlider(
+                                    floatingSlidersContainer.getChildAt(index)
+                                )
                             }
                         }
                         .launchIn(this)
@@ -68,6 +69,11 @@
             }
         }
     }
+
+    private fun VolumeDialogSliderComponent.bindSlider(sliderContainer: View) {
+        sliderViewBinder().bind(sliderContainer)
+        sliderTouchesViewBinder().bind(sliderContainer)
+    }
 }
 
 private fun ViewGroup.ensureChildCount(@LayoutRes viewLayoutId: Int, count: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderTouchesViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderTouchesViewModel.kt
new file mode 100644
index 0000000..144c75d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderTouchesViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.view.MotionEvent
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInputEventsInteractor
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class VolumeDialogSliderTouchesViewModel
+@AssistedInject
+constructor(private val interactor: VolumeDialogSliderInputEventsInteractor) {
+
+    fun onTouchEvent(event: MotionEvent) {
+        interactor.onTouchEvent(event)
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): VolumeDialogSliderTouchesViewModel
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
index 6e9b24f..4ca84c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
@@ -37,7 +37,6 @@
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -63,22 +62,18 @@
             Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java)
                 .allowMainThreadQueries()
                 .build()
+        onTeardown { database.close() }
         CommunalDatabase.setInstance(database)
 
         dao = database.communalWidgetDao()
         backupUtils = CommunalBackupUtils(context)
 
         backupDataFile = File(context.cacheDir, "backup_data_file")
+        onTeardown { backupDataFile.delete() }
 
         underTest = CommunalBackupHelper(UserHandle.SYSTEM, backupUtils)
     }
 
-    @After
-    fun teardown() {
-        backupDataFile.delete()
-        database.close()
-    }
-
     @Test
     @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
     fun backupAndRestoreCommunalHub() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index a9b6dd1..c6f01ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -16,6 +16,15 @@
 
 package com.android.systemui.keyboard.shortcut.ui.viewmodel
 
+import android.content.Context
+import android.content.Context.INPUT_SERVICE
+import android.hardware.input.fakeInputManager
+import android.os.SystemClock
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_A
+import android.view.KeyEvent.META_CTRL_ON
+import android.view.KeyEvent.META_META_ON
+import androidx.compose.ui.input.key.KeyEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -24,24 +33,43 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shortcutCustomizationViewModelFactory
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.userTracker
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ShortcutCustomizationViewModelTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos().also { it.testCase = this }
+    private val mockUserContext: Context = mock()
+    private val kosmos =
+        Kosmos().also {
+            it.testCase = this
+            it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+        }
     private val testScope = kosmos.testScope
+    private val inputManager = kosmos.fakeInputManager.inputManager
+    private val helper = kosmos.shortcutHelperTestHelper
     private val viewModel = kosmos.shortcutCustomizationViewModelFactory.create()
 
+    @Before
+    fun setup() {
+        helper.showFromActivity()
+        whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+    }
+
     @Test
     fun uiState_inactiveByDefault() {
         testScope.runTest {
@@ -79,11 +107,111 @@
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
             viewModel.onAddShortcutDialogShown()
-            viewModel.onAddShortcutDialogDismissed()
+            viewModel.onDialogDismissed()
             assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive)
         }
     }
 
+    @Test
+    fun uiState_pressedKeys_emptyByDefault() {
+        testScope.runTest {
+            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).pressedKeys)
+                .isEmpty()
+        }
+    }
+
+    @Test
+    fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
+        testScope.runTest {
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
+
+            assertThat(isHandled).isTrue()
+        }
+    }
+
+    @Test
+    fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
+        testScope.runTest {
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed)
+
+            assertThat(isHandled).isFalse()
+        }
+    }
+
+    @Test
+    fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
+        testScope.runTest {
+            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
+            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+
+            // Note that Action Key is excluded as it's already displayed on the UI
+            assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).pressedKeys)
+                .containsExactly(ShortcutKey.Text("Ctrl"), ShortcutKey.Text("A"))
+        }
+    }
+
+    @Test
+    fun uiState_pressedKeys_resetsToEmptyListAfterDialogIsDismissedAndReopened() {
+        testScope.runTest {
+            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
+            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+
+            // Note that Action Key is excluded as it's already displayed on the UI
+            assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).pressedKeys)
+                .containsExactly(ShortcutKey.Text("Ctrl"), ShortcutKey.Text("A"))
+
+            // Close the dialog and show it again
+            viewModel.onDialogDismissed()
+            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+            assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).pressedKeys)
+                .isEmpty()
+        }
+    }
+
+    private val keyDownEventWithoutActionKeyPressed =
+        KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ META_CTRL_ON,
+            )
+        )
+
+    private val keyDownEventWithActionKeyPressed =
+        KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ META_CTRL_ON or META_META_ON,
+            )
+        )
+
+    private val keyUpEventWithActionKeyPressed =
+        KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ 0,
+            )
+        )
+
     private val standardAddShortcutRequest =
         ShortcutCustomizationRequestInfo.Add(
             label = "Standard shortcut",
@@ -95,7 +223,6 @@
         ShortcutCustomizationUiState.AddShortcutDialog(
             shortcutLabel = "Standard shortcut",
             shouldShowErrorMessage = false,
-            isValidKeyCombination = false,
             defaultCustomShortcutModifierKey =
                 ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
             isDialogShowing = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index e313a05..d8a23d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -45,6 +45,7 @@
 import android.graphics.drawable.AnimationDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
@@ -551,12 +552,26 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_REENTRANT_DISMISS)
+    public void testCanDismiss_immediately() throws Exception {
+        ExpandableNotificationRow row =
+                mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
+        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+                .thenReturn(true);
+        row.performDismiss(false);
+        verify(mNotificationTestHelper.getOnUserInteractionCallback())
+                .registerFutureDismissal(any(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_REENTRANT_DISMISS)
     public void testCanDismiss() throws Exception {
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
         when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
                 .thenReturn(true);
         row.performDismiss(false);
+        TestableLooper.get(this).processAllMessages();
         verify(mNotificationTestHelper.getOnUserInteractionCallback())
                 .registerFutureDismissal(any(), anyInt());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index b142fc2..c9f8463 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -607,7 +607,6 @@
                 mShadeController,
                 mWindowRootViewVisibilityInteractor,
                 mStatusBarKeyguardViewManager,
-                () -> mStatusBarLongPressGestureDetector,
                 mViewMediatorCallback,
                 mInitController,
                 new Handler(TestableLooper.get(this).getLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 856333e..aad8b4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -2707,6 +2707,44 @@
                 eq(BubbleLogger.Event.BUBBLE_BAR_EXPANDED));
     }
 
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    public void testEventLogging_bubbleBar_openOverflow() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+
+        clearInvocations(mBubbleLogger);
+        mBubbleController.expandStackAndSelectBubbleFromLauncher(BubbleOverflow.KEY, 0);
+        verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_SELECTED);
+        verifyNoMoreInteractions(mBubbleLogger);
+    }
+
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    public void testEventLogging_bubbleBar_fromOverflowToBar() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+
+        // Dismiss the bubble so it's in the overflow
+        mBubbleController.removeBubble(
+                mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        Bubble overflowBubble = mBubbleData.getOverflowBubbleWithKey(mRow.getKey());
+        assertThat(overflowBubble).isNotNull();
+
+        // Promote overflow bubble and check that it is logged
+        mBubbleController.promoteBubbleFromOverflow(overflowBubble);
+        verify(mBubbleLogger).log(eqBubbleWithKey(overflowBubble.getKey()),
+                eq(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR));
+    }
+
     /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt
similarity index 77%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt
index a321fb0..379c008 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package android.content
 
-/** {@hide} */
-parcelable ForensicEvent;
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.clipboardManager by Kosmos.Fixture { mock<ClipboardManager>() }
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt
similarity index 61%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt
index a321fb0..3ce1195 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt
@@ -14,7 +14,12 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package com.android.systemui.development.data.repository
 
-/** {@hide} */
-parcelable ForensicEvent;
+import android.os.userManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.settings.fakeGlobalSettings
+
+val Kosmos.developmentSettingRepository by
+    Kosmos.Fixture { DevelopmentSettingRepository(fakeGlobalSettings, userManager, testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt
new file mode 100644
index 0000000..aa4dd18
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.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.development.domain.interactor
+
+import android.content.clipboardManager
+import android.content.res.mainResources
+import com.android.systemui.development.data.repository.developmentSettingRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.user.data.repository.userRepository
+
+val Kosmos.buildNumberInteractor by
+    Kosmos.Fixture {
+        BuildNumberInteractor(
+            developmentSettingRepository,
+            mainResources,
+            userRepository,
+            { clipboardManager },
+            testDispatcher,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt
new file mode 100644
index 0000000..c827311
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.development.ui.viewmodel
+
+import com.android.systemui.development.domain.interactor.buildNumberInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.buildNumberViewModelFactory by
+    Kosmos.Fixture {
+        object : BuildNumberViewModel.Factory {
+            override fun create(): BuildNumberViewModel {
+                return BuildNumberViewModel(buildNumberInteractor)
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index 0e5edb7..2e80293 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor
 
@@ -26,5 +27,6 @@
             qsColumnsViewModelFactory,
             paginatedGridInteractor,
             inFirstPageViewModel,
+            buildNumberViewModelFactory,
         )
     }
diff --git a/core/java/android/security/forensic/ForensicEvent.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepositoryKosmos.kt
similarity index 72%
copy from core/java/android/security/forensic/ForensicEvent.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepositoryKosmos.kt
index a321fb0..1e8dfa1 100644
--- a/core/java/android/security/forensic/ForensicEvent.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepositoryKosmos.kt
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package android.security.forensic;
+package com.android.systemui.volume.dialog.sliders.data.repository
 
-/** {@hide} */
-parcelable ForensicEvent;
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.volumeDialogSliderTouchEventsRepository by
+    Kosmos.Fixture { VolumeDialogSliderTouchEventsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorKosmo.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorKosmo.kt
new file mode 100644
index 0000000..39ae5aa
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorKosmo.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.volume.dialog.sliders.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogCallbacksInteractor
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.data.repository.volumeDialogSliderTouchEventsRepository
+
+val Kosmos.volumeDialogSliderInputEventsInteractor: VolumeDialogSliderInputEventsInteractor by
+    Kosmos.Fixture {
+        VolumeDialogSliderInputEventsInteractor(
+            applicationCoroutineScope,
+            volumeDialogCallbacksInteractor,
+            volumeDialogVisibilityInteractor,
+            volumeDialogSliderTouchEventsRepository,
+        )
+    }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
index 619c8e3..7ca9239 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
@@ -142,33 +142,31 @@
         }
 
         /**
-         * Configure the given system property as immutable for the duration of the test.
-         * Read access to the key is allowed, and write access will fail. When {@code value} is
-         * {@code null}, the value is left as undefined.
-         *
-         * All properties in the {@code debug.*} namespace are automatically mutable, with no
-         * developer action required.
-         *
-         * Has no effect on non-Ravenwood environments.
+         * @deprecated Use {@link RavenwoodRule.Builder#setSystemPropertyImmutable(String, Object)}
          */
+        @Deprecated
         public Builder setSystemPropertyImmutable(@NonNull String key,
                 @Nullable Object value) {
+            return this;
+        }
+
+        /**
+         * @deprecated Use {@link RavenwoodRule.Builder#setSystemPropertyMutable(String, Object)}
+         */
+        @Deprecated
+        public Builder setSystemPropertyMutable(@NonNull String key,
+                @Nullable Object value) {
+            return this;
+        }
+
+        Builder setSystemPropertyImmutableReal(@NonNull String key,
+                @Nullable Object value) {
             mConfig.mSystemProperties.setValue(key, value);
             mConfig.mSystemProperties.setAccessReadOnly(key);
             return this;
         }
 
-        /**
-         * Configure the given system property as mutable for the duration of the test.
-         * Both read and write access to the key is allowed, and its value will be reset between
-         * each test. When {@code value} is {@code null}, the value is left as undefined.
-         *
-         * All properties in the {@code debug.*} namespace are automatically mutable, with no
-         * developer action required.
-         *
-         * Has no effect on non-Ravenwood environments.
-         */
-        public Builder setSystemPropertyMutable(@NonNull String key,
+        Builder setSystemPropertyMutableReal(@NonNull String key,
                 @Nullable Object value) {
             mConfig.mSystemProperties.setValue(key, value);
             mConfig.mSystemProperties.setAccessReadWrite(key);
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index f7acd90..5681a90 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -152,7 +152,7 @@
          * Has no effect on non-Ravenwood environments.
          */
         public Builder setSystemPropertyImmutable(@NonNull String key, @Nullable Object value) {
-            mBuilder.setSystemPropertyImmutable(key, value);
+            mBuilder.setSystemPropertyImmutableReal(key, value);
             return this;
         }
 
@@ -167,7 +167,7 @@
          * Has no effect on non-Ravenwood environments.
          */
         public Builder setSystemPropertyMutable(@NonNull String key, @Nullable Object value) {
-            mBuilder.setSystemPropertyMutable(key, value);
+            mBuilder.setSystemPropertyMutableReal(key, value);
             return this;
         }
 
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index e59bb42..4f632c9 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -353,7 +353,10 @@
     private static void addAutofillableIds(@NonNull ViewNode node,
             @NonNull ArrayList<AutofillId> ids, boolean autofillableOnly) {
         if (!autofillableOnly || node.getAutofillType() != View.AUTOFILL_TYPE_NONE) {
-            ids.add(node.getAutofillId());
+            AutofillId id = node.getAutofillId();
+            if (id != null) {
+                ids.add(id);
+            }
         }
         final int size = node.getChildCount();
         for (int i = 0; i < size; i++) {
diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
index dbeca82a..2d3782f 100644
--- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
+++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
@@ -16,6 +16,8 @@
 
 package com.android.server.companion.securechannel;
 
+import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN;
+
 import android.annotation.NonNull;
 import android.content.Context;
 import android.os.Build;
@@ -67,7 +69,7 @@
     private D2DConnectionContextV1 mConnectionContext;
 
     private String mAlias;
-    private int mVerificationResult;
+    private int mVerificationResult = FLAG_FAILURE_UNKNOWN;
     private boolean mPskVerified;
 
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 679c7ac..3ce6451 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -16,6 +16,10 @@
 
 package com.android.server.accounts;
 
+import static android.Manifest.permission.COPY_ACCOUNTS;
+import static android.Manifest.permission.REMOVE_ACCOUNTS;
+import static android.app.admin.flags.Flags.splitCreateManagedProfileEnabled;
+
 import android.Manifest;
 import android.accounts.AbstractAccountAuthenticator;
 import android.accounts.Account;
@@ -1739,9 +1743,11 @@
     public void copyAccountToUser(final IAccountManagerResponse response, final Account account,
             final int userFrom, int userTo) {
         int callingUid = Binder.getCallingUid();
-        if (isCrossUser(callingUid, UserHandle.USER_ALL)) {
+        if (isCrossUser(callingUid, UserHandle.USER_ALL)
+                && !hasCopyAccountsPermission()) {
             throw new SecurityException("Calling copyAccountToUser requires "
-                    + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+                    + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+                    + " or " + COPY_ACCOUNTS);
         }
         final UserAccounts fromAccounts = getUserAccounts(userFrom);
         final UserAccounts toAccounts = getUserAccounts(userTo);
@@ -1793,6 +1799,12 @@
         }
     }
 
+    private boolean hasCopyAccountsPermission() {
+        return splitCreateManagedProfileEnabled()
+                && mContext.checkCallingOrSelfPermission(COPY_ACCOUNTS)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
     @Override
     public boolean accountAuthenticated(final Account account) {
         final int callingUid = Binder.getCallingUid();
@@ -2346,7 +2358,8 @@
         UserHandle user = UserHandle.of(userId);
         if (!isAccountManagedByCaller(account.type, callingUid, user.getIdentifier())
                 && !isSystemUid(callingUid)
-                && !isProfileOwner(callingUid)) {
+                && !isProfileOwner(callingUid)
+                && !hasRemoveAccountsPermission()) {
             String msg = String.format(
                     "uid %s cannot remove accounts of type: %s",
                     callingUid,
@@ -2408,6 +2421,12 @@
         }
     }
 
+    private boolean hasRemoveAccountsPermission() {
+        return splitCreateManagedProfileEnabled()
+                && mContext.checkCallingOrSelfPermission(REMOVE_ACCOUNTS)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
     @Override
     public boolean removeAccountExplicitly(Account account) {
         final int callingUid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 3f540ad..ab3ab15 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -115,6 +115,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__REQUEST_TYPE__BIND;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__REQUEST_TYPE__START;
+import static com.android.media.flags.Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
@@ -9319,6 +9320,46 @@
         }
     }
 
+    /**
+     * Handles notifications from MediaSessionService about inactive media foreground services.
+     * This method evaluates the provided information and determines whether to stop the
+     * corresponding foreground service.
+     *
+     * @param packageName The package name of the app running the foreground service.
+     * @param userId The user ID associated with the foreground service.
+     * @param notificationId The ID of the media notification associated with the foreground
+     *                      service.
+     */
+    void notifyInactiveMediaForegroundServiceLocked(@NonNull String packageName,
+            @UserIdInt int userId, int notificationId) {
+        if (!enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+            return;
+        }
+
+        final ServiceMap smap = mServiceMap.get(userId);
+        if (smap == null) {
+            return;
+        }
+        final int serviceSize = smap.mServicesByInstanceName.size();
+        for (int i = 0; i < serviceSize; i++) {
+            final ServiceRecord sr = smap.mServicesByInstanceName.valueAt(i);
+            if (sr.appInfo.packageName.equals(packageName) && sr.isForeground) {
+                if (sr.foregroundServiceType
+                        == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+                        && sr.foregroundId == notificationId) {
+                    if (DEBUG_FOREGROUND_SERVICE) {
+                        Slog.d(TAG, "Forcing media foreground service to background for package "
+                                + packageName);
+                    }
+                    setServiceForegroundInnerLocked(sr, /* id */ 0,
+                            /* notification */ null, /* flags */ 0,
+                            /* foregroundServiceType */ 0, /* callingUidStart */ 0);
+                }
+            }
+        }
+    }
+
+
     private static void getClientPackages(ServiceRecord sr, ArraySet<String> output) {
         var connections = sr.getConnections();
         for (int conni = connections.size() - 1; conni >= 0; conni--) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e166807..5c5361b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17923,6 +17923,15 @@
         }
 
         @Override
+        public void notifyInactiveMediaForegroundService(@NonNull String packageName,
+                @UserIdInt int userId, int notificationId) {
+            synchronized (ActivityManagerService.this) {
+                mServices.notifyInactiveMediaForegroundServiceLocked(packageName, userId,
+                        notificationId);
+            }
+        }
+
+        @Override
         public ArraySet<String> getClientPackages(String servicePackageName) {
             synchronized (ActivityManagerService.this) {
                 return mServices.getClientPackagesLocked(servicePackageName);
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 02e2c39..d3d3fc9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -39,6 +39,8 @@
 import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_LOW;
 import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_MODERATE;
 import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_NORMAL;
+import static com.android.media.flags.Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange;
+import static com.android.media.flags.Flags.FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE;
 import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -445,6 +447,8 @@
                     return runCapabilities(pw);
                 case "set-app-zygote-preload-timeout":
                     return runSetAppZygotePreloadTimeout(pw);
+                case "set-media-foreground-service":
+                    return runSetMediaForegroundService(pw);
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -454,6 +458,48 @@
         return -1;
     }
 
+    int runSetMediaForegroundService(PrintWriter pw) throws RemoteException {
+        mInternal.enforceCallingPermission(
+                android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE,
+                "runSetMediaForegroundService()");
+        final PrintWriter err = getErrPrintWriter();
+        if (!enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+            err.println("Error: flag "
+                    + FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE
+                    + " not enabled");
+            return -1;
+        }
+        int userId = UserHandle.USER_CURRENT;
+        final String cmd = getNextArgRequired();
+        if ("inactive".equals(cmd)) {
+            String opt;
+            while ((opt = getNextOption()) != null) {
+                if (opt.equals("--user")) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    if (userId == UserHandle.USER_ALL) {
+                        err.println(
+                                "Error: Can't set media fgs inactive with user 'all'");
+                        return -1;
+                    }
+                } else {
+                    err.println("Error: Unknown option: " + opt);
+                    return -1;
+                }
+            }
+            final String pkgName = getNextArgRequired();
+            final int notificationId = Integer.parseInt(getNextArgRequired());
+            if (notificationId == 0) {
+                err.println("Error: notification id cannot be zero");
+                return -1;
+            }
+            mInternal.mInternal.notifyInactiveMediaForegroundService(pkgName,
+                    userId, notificationId);
+            return 0;
+        }
+        err.println("Error: Unknown set-media-foreground-service command: " + cmd);
+        return -1;
+    }
+
     int runSetAppZygotePreloadTimeout(PrintWriter pw) throws RemoteException {
         final String timeout = getNextArgRequired();
         final int timeoutMs = Integer.parseInt(timeout);
@@ -4637,6 +4683,9 @@
             pw.println("         --protobuf: format output using protobuffer");
             pw.println("  set-app-zygote-preload-timeout <TIMEOUT_IN_MS>");
             pw.println("         Set the timeout for preloading code in the app-zygote");
+            pw.println("  set-media-foreground-service inactive [--user USER_ID]"
+                    + " <PACKAGE> <NOTIFICATION_ID>");
+            pw.println("         Set an app's media foreground service inactive.");
             Intent.printIntentArgsHelp(pw, "");
         }
     }
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 3913d2f..961022b 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -634,12 +634,20 @@
         }
 
         final ApplicationStartInfo info = new ApplicationStartInfo(raw);
+        int uid = raw.getRealUid();
 
-        AppStartInfoContainer container = mData.get(raw.getPackageName(), raw.getRealUid());
+        // Isolated process starts won't be reasonably accessible if stored by their uid, don't
+        // store them.
+        if (com.android.server.am.Flags.appStartInfoIsolatedProcess()
+                && UserHandle.isIsolated(uid)) {
+            return null;
+        }
+
+        AppStartInfoContainer container = mData.get(raw.getPackageName(), uid);
         if (container == null) {
             container = new AppStartInfoContainer(mAppStartInfoHistoryListSize);
-            container.mUid = info.getRealUid();
-            mData.put(raw.getPackageName(), raw.getRealUid(), container);
+            container.mUid = uid;
+            mData.put(raw.getPackageName(), uid, container);
         }
         container.addStartInfoLocked(info);
 
@@ -1010,6 +1018,17 @@
                             new AppStartInfoContainer(mAppStartInfoHistoryListSize);
                     int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS,
                             pkgName);
+
+                    // If the isolated process flag is enabled and the uid is that of an isolated
+                    // process, then break early so that the container will not be added to mData.
+                    // This is expected only as a one time mitigation, records added after this flag
+                    // is enabled should always return false for isIsolated and thereby always
+                    // continue on.
+                    if (com.android.server.am.Flags.appStartInfoIsolatedProcess()
+                            && UserHandle.isIsolated(uid)) {
+                        break;
+                    }
+
                     synchronized (mLock) {
                         mData.put(pkgName, uid, container);
                     }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index ef5296e..78c4f74 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -257,6 +257,7 @@
         "wear_systems",
         "wear_sysui",
         "wear_system_managed_surfaces",
+        "wear_watchfaces",
         "window_surfaces",
         "windowing_frontend",
         "xr",
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index c59c40f..6d247d2 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -270,3 +270,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "app_start_info_isolated_process"
+    namespace: "system_performance"
+    description: "Adjust handling of isolated process records to be discarded."
+    bug: "374032823"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index edad247..dcc95e2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW;
 import static android.content.PermissionChecker.PERMISSION_GRANTED;
 import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
 import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT;
 import static android.view.KeyEvent.KEYCODE_UNKNOWN;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -136,6 +137,7 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.DisplayThread;
 import com.android.server.LocalServices;
+import com.android.server.SystemService;
 import com.android.server.Watchdog;
 import com.android.server.input.InputManagerInternal.LidSwitchCallback;
 import com.android.server.input.debug.FocusEventDebugView;
@@ -3031,6 +3033,11 @@
         return mKeyGestureController.getAppLaunchBookmarks();
     }
 
+    @Override
+    public void resetLockedModifierState() {
+        mNative.resetLockedModifierState();
+    }
+
     private void handleCurrentUserChanged(@UserIdInt int userId) {
         mCurrentUserId = userId;
         mKeyGestureController.setCurrentUserId(userId);
@@ -3391,6 +3398,37 @@
         }
     }
 
+    /**
+     * {@link SystemService} used to publish and manage the lifecycle of {@link InputManagerService}
+     */
+    public static final class Lifecycle extends SystemService {
+
+        private final InputManagerService mService;
+
+        public Lifecycle(@NonNull Context context) {
+            super(context);
+            mService = new InputManagerService(context);
+        }
+
+        @Override
+        public void onStart() {
+            publishBinderService(Context.INPUT_SERVICE, mService,
+                    /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            // Called on ActivityManager thread.
+            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+                mService.systemRunning();
+            }
+        }
+
+        public InputManagerService getService() {
+            return mService;
+        }
+    }
+
     private final class LocalService extends InputManagerInternal {
         @Override
         public void setDisplayViewports(List<DisplayViewport> viewports) {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 8903c27..728e440 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -98,6 +98,8 @@
 
     void toggleCapsLock(int deviceId);
 
+    void resetLockedModifierState();
+
     void displayRemoved(int displayId);
 
     void setInputDispatchMode(boolean enabled, boolean frozen);
@@ -370,6 +372,9 @@
         public native void toggleCapsLock(int deviceId);
 
         @Override
+        public native void resetLockedModifierState();
+
+        @Override
         public native void displayRemoved(int displayId);
 
         @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d8483f7..b7af9a4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1310,7 +1310,7 @@
         // Do not reset the default (current) IME when it is a 3rd-party IME
         String selectedMethodId = bindingController.getSelectedMethodId();
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
-        if (selectedMethodId != null
+        if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null
                 && !settings.getMethodMap().get(selectedMethodId).isSystem()) {
             return;
         }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
index 0fdd0ae..5248a05 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
@@ -237,15 +237,16 @@
 
         if (message.isBroadcastMessage()) {
             if (message.isReliable()) {
-                Log.e(TAG, "Received reliable broadcast message from " + message.getNanoAppId());
+                Log.e(TAG, "Received reliable broadcast message from 0x"
+                        + Long.toHexString(message.getNanoAppId()));
                 return ErrorCode.PERMANENT_ERROR;
             }
 
             // Broadcast messages shouldn't be sent with any permissions tagged per CHRE API
             // requirements.
             if (!messagePermissions.isEmpty()) {
-                Log.e(TAG, "Received broadcast message with permissions from "
-                        + message.getNanoAppId());
+                Log.e(TAG, "Received broadcast message with permissions from 0x"
+                        + Long.toHexString(message.getNanoAppId()));
                 return ErrorCode.PERMANENT_ERROR;
             }
 
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 946e896..7c9d9c5 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -326,8 +326,15 @@
         }
 
         if (Flags.offloadApi()) {
-            mHubInfoRegistry = new HubInfoRegistry(mContextHubWrapper);
-            Log.i(TAG, "Enabling generic offload API");
+            HubInfoRegistry registry;
+            try {
+                registry = new HubInfoRegistry(mContextHubWrapper);
+                Log.i(TAG, "Enabling generic offload API");
+            } catch (UnsupportedOperationException e) {
+                registry = null;
+                Log.w(TAG, "Generic offload API not supported, disabling");
+            }
+            mHubInfoRegistry = registry;
         } else {
             mHubInfoRegistry = null;
             Log.i(TAG, "Disabling generic offload API");
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 00bab8a..8495b6c 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -28,6 +28,7 @@
 import android.media.quality.MediaQualityContract;
 import android.media.quality.ParamCapability;
 import android.media.quality.PictureProfile;
+import android.media.quality.PictureProfileHandle;
 import android.media.quality.SoundProfile;
 import android.os.PersistableBundle;
 import android.util.Log;
@@ -249,6 +250,11 @@
         }
 
         @Override
+        public PictureProfileHandle getPictureProfileHandle(String id) {
+            return null;
+        }
+
+        @Override
         public SoundProfile createSoundProfile(SoundProfile pp) {
             // TODO: implement
             return pp;
diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS
index e8fc577..62b89f32 100644
--- a/services/core/java/com/android/server/pm/OWNERS
+++ b/services/core/java/com/android/server/pm/OWNERS
@@ -38,19 +38,19 @@
 per-file SaferIntentUtils.java = topjohnwu@google.com
 
 # shortcuts
-per-file LauncherAppsService.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShareTargetInfo.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutBitmapSaver.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutDumpFiles.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutLauncher.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutNonPersistentUser.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutPackage.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutPackageInfo.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutPackageItem.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutParser.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutRequestPinProcessor.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutService.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
-per-file ShortcutUser.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com
+per-file LauncherAppsService.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShareTargetInfo.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutBitmapSaver.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutDumpFiles.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutLauncher.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutNonPersistentUser.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutPackage.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutPackageInfo.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutPackageItem.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutParser.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutRequestPinProcessor.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutService.java = pinyaoting@google.com, sunnygoyal@google.com
+per-file ShortcutUser.java = pinyaoting@google.com, sunnygoyal@google.com
 
 # background install control service
-per-file BackgroundInstall* = file:BACKGROUND_INSTALL_OWNERS
\ No newline at end of file
+per-file BackgroundInstall* = file:BACKGROUND_INSTALL_OWNERS
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 505b7e6..58986dc 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -1092,6 +1092,12 @@
         final boolean isInstallDpcPackagesPermissionGranted = (snapshot.checkUidPermission(
                 android.Manifest.permission.INSTALL_DPC_PACKAGES, mInstallerUid)
                 == PackageManager.PERMISSION_GRANTED);
+        boolean isInstallDependencyPackagesPermissionGranted = false;
+        if (Flags.sdkDependencyInstaller()) {
+            isInstallDependencyPackagesPermissionGranted = (snapshot.checkUidPermission(
+                    android.Manifest.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES, mInstallerUid)
+                    == PackageManager.PERMISSION_GRANTED);
+        }
         // Also query the package uid for archived packages, so that the user confirmation
         // dialog can be displayed for updating archived apps.
         final int targetPackageUid = snapshot.getPackageUid(packageName,
@@ -1113,10 +1119,18 @@
         final boolean isSelfUpdate = targetPackageUid == mInstallerUid;
         final boolean isEmergencyInstall =
                 isEmergencyInstallerEnabled(packageName, snapshot, userId, mInstallerUid);
+        boolean isSdkOrStaticLibraryInstall = false;
+        synchronized (mLock) {
+            if (mPackageLite != null) {
+                isSdkOrStaticLibraryInstall =
+                        mPackageLite.isIsSdkLibrary() || mPackageLite.isIsStaticLibrary();
+            }
+        }
         final boolean isPermissionGranted = isInstallPermissionGranted
                 || (isUpdatePermissionGranted && isUpdate)
                 || (isSelfUpdatePermissionGranted && isSelfUpdate)
-                || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver);
+                || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver)
+                || (isInstallDependencyPackagesPermissionGranted && isSdkOrStaticLibraryInstall);
         final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID);
         final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID);
         final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID);
diff --git a/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java b/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java
index b8a4a9c..6798a61 100644
--- a/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java
+++ b/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java
@@ -16,8 +16,11 @@
 
 package com.android.server.security.authenticationpolicy;
 
+import static android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE;
+
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
 
+import android.annotation.EnforcePermission;
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -32,9 +35,14 @@
 import android.hardware.biometrics.events.AuthenticationSucceededInfo;
 import android.os.Build;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.security.authenticationpolicy.AuthenticationPolicyManager;
+import android.security.authenticationpolicy.DisableSecureLockDeviceParams;
+import android.security.authenticationpolicy.EnableSecureLockDeviceParams;
+import android.security.authenticationpolicy.IAuthenticationPolicyService;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseIntArray;
@@ -74,6 +82,7 @@
     private final KeyguardManager mKeyguardManager;
     private final WindowManagerInternal mWindowManager;
     private final UserManagerInternal mUserManager;
+    private SecureLockDeviceServiceInternal mSecureLockDeviceService;
     @VisibleForTesting
     final SparseIntArray mFailedAttemptsForUser = new SparseIntArray();
     private final SparseLongArray mLastLockedTimestamp = new SparseLongArray();
@@ -94,10 +103,16 @@
         mWindowManager = Objects.requireNonNull(
                 LocalServices.getService(WindowManagerInternal.class));
         mUserManager = Objects.requireNonNull(LocalServices.getService(UserManagerInternal.class));
+        if (android.security.Flags.secureLockdown()) {
+            mSecureLockDeviceService = Objects.requireNonNull(
+                    LocalServices.getService(SecureLockDeviceServiceInternal.class));
+        }
     }
 
     @Override
-    public void onStart() {}
+    public void onStart() {
+        publishBinderService(Context.AUTHENTICATION_POLICY_SERVICE, mService);
+    }
 
     @Override
     public void onBootPhase(int phase) {
@@ -294,4 +309,36 @@
         // next successful primary or biometric auth happens
         mLastLockedTimestamp.put(userId, SystemClock.elapsedRealtime());
     }
+
+    private final IBinder mService = new IAuthenticationPolicyService.Stub() {
+        /**
+         * @see AuthenticationPolicyManager#enableSecureLockDevice(EnableSecureLockDeviceParams)
+         * @param params EnableSecureLockDeviceParams for caller to supply params related
+         *               to the secure lock device request
+         * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the Secure
+         * Lock Device request
+         */
+        @Override
+        @EnforcePermission(MANAGE_SECURE_LOCK_DEVICE)
+        @AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus
+        public int enableSecureLockDevice(EnableSecureLockDeviceParams params) {
+            enableSecureLockDevice_enforcePermission();
+            return mSecureLockDeviceService.enableSecureLockDevice(params);
+        }
+
+        /**
+         * @see AuthenticationPolicyManager#disableSecureLockDevice(DisableSecureLockDeviceParams)
+         * @param params @DisableSecureLockDeviceParams for caller to supply params related
+         *               to the secure lock device request
+         * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the Secure
+         * Lock Device request
+         */
+        @Override
+        @EnforcePermission(MANAGE_SECURE_LOCK_DEVICE)
+        @AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus
+        public int disableSecureLockDevice(DisableSecureLockDeviceParams params) {
+            disableSecureLockDevice_enforcePermission();
+            return mSecureLockDeviceService.disableSecureLockDevice(params);
+        }
+    };
 }
diff --git a/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java
new file mode 100644
index 0000000..7b89723
--- /dev/null
+++ b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security.authenticationpolicy;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.security.authenticationpolicy.AuthenticationPolicyManager;
+import android.security.authenticationpolicy.AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus;
+import android.security.authenticationpolicy.AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus;
+import android.security.authenticationpolicy.DisableSecureLockDeviceParams;
+import android.security.authenticationpolicy.EnableSecureLockDeviceParams;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+
+/**
+ * System service for remotely calling secure lock on the device.
+ *
+ * Callers will access this class via
+ * {@link com.android.server.security.authenticationpolicy.AuthenticationPolicyService}.
+ *
+ * @see AuthenticationPolicyService
+ * @see AuthenticationPolicyManager#enableSecureLockDevice
+ * @see AuthenticationPolicyManager#disableSecureLockDevice
+ * @hide
+ */
+public class SecureLockDeviceService extends SecureLockDeviceServiceInternal {
+    private static final String TAG = "SecureLockDeviceService";
+    private final Context mContext;
+
+    public SecureLockDeviceService(@NonNull Context context) {
+        mContext = context;
+    }
+
+    private void start() {
+        // Expose private service for system components to use.
+        LocalServices.addService(SecureLockDeviceServiceInternal.class, this);
+    }
+
+    /**
+     * @see AuthenticationPolicyManager#enableSecureLockDevice
+     * @param params EnableSecureLockDeviceParams for caller to supply params related
+     *               to the secure lock device request
+     * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the
+     * secure lock device request
+     *
+     * @hide
+     */
+    @Override
+    @EnableSecureLockDeviceRequestStatus
+    public int enableSecureLockDevice(EnableSecureLockDeviceParams params) {
+        // (1) Call into system_server to lock device, configure allowed auth types
+        // for secure lock
+        // TODO: lock device, configure allowed authentication types for device entry
+        // (2) Call into framework to configure secure lock 2FA lockscreen
+        // update, UI & string updates
+        // TODO: implement 2FA lockscreen when SceneContainerFlag.isEnabled()
+        // TODO: implement 2FA lockscreen when !SceneContainerFlag.isEnabled()
+        // (3) Call into framework to configure keyguard security updates
+        // TODO: implement security updates
+        return AuthenticationPolicyManager.ERROR_UNSUPPORTED;
+    }
+
+    /**
+     * @see AuthenticationPolicyManager#disableSecureLockDevice
+     * @param params @DisableSecureLockDeviceParams for caller to supply params related
+     *               to the secure lock device request
+     * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the
+     * secure lock device request
+     *
+     * @hide
+     */
+    @Override
+    @DisableSecureLockDeviceRequestStatus
+    public int disableSecureLockDevice(DisableSecureLockDeviceParams params) {
+        // (1) Call into system_server to reset allowed auth types
+        // TODO: reset allowed authentication types for device entry;
+        // (2) Call into framework to disable secure lock 2FA lockscreen, reset UI
+        // & string updates
+        // TODO: implement reverting to normal lockscreen when SceneContainerFlag.isEnabled()
+        // TODO: implement reverting to normal lockscreen when !SceneContainerFlag.isEnabled()
+        // (3) Call into framework to revert keyguard security updates
+        // TODO: implement reverting security updates
+        return AuthenticationPolicyManager.ERROR_UNSUPPORTED;
+    }
+
+    /**
+     * System service lifecycle.
+     */
+    public static final class Lifecycle extends SystemService {
+        private final SecureLockDeviceService mService;
+
+        public Lifecycle(@NonNull Context context) {
+            super(context);
+            mService = new SecureLockDeviceService(context);
+        }
+
+        @Override
+        public void onStart() {
+            Slog.i(TAG, "Starting SecureLockDeviceService");
+            mService.start();
+            Slog.i(TAG, "Started SecureLockDeviceService");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java
new file mode 100644
index 0000000..b903709
--- /dev/null
+++ b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java
@@ -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.server.security.authenticationpolicy;
+
+import android.security.authenticationpolicy.AuthenticationPolicyManager;
+import android.security.authenticationpolicy.AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus;
+import android.security.authenticationpolicy.AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus;
+import android.security.authenticationpolicy.DisableSecureLockDeviceParams;
+import android.security.authenticationpolicy.EnableSecureLockDeviceParams;
+
+/**
+ * Local system service interface for {@link SecureLockDeviceService}.
+ *
+ * <p>No permission / argument checks will be performed inside.
+ * Callers must check the calling app permission and the calling package name.
+ *
+ * @hide
+ */
+public abstract class SecureLockDeviceServiceInternal {
+    private static final String TAG = "SecureLockDeviceServiceInternal";
+
+    /**
+     * @see AuthenticationPolicyManager#enableSecureLockDevice(EnableSecureLockDeviceParams)
+     * @param params EnableSecureLockDeviceParams for caller to supply params related
+     *               to the secure lock request
+     * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the
+     * secure lock request
+     */
+    @EnableSecureLockDeviceRequestStatus
+    public abstract int enableSecureLockDevice(EnableSecureLockDeviceParams params);
+
+    /**
+     * @see AuthenticationPolicyManager#disableSecureLockDevice(DisableSecureLockDeviceParams)
+     * @param params @DisableSecureLockDeviceParams for caller to supply params related
+     *               to the secure lock device request
+     * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the
+     * secure lock device request
+     */
+    @DisableSecureLockDeviceRequestStatus
+    public abstract int disableSecureLockDevice(DisableSecureLockDeviceParams params);
+}
diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java
deleted file mode 100644
index 2be068f..0000000
--- a/services/core/java/com/android/server/security/forensic/ForensicService.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * 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.security.forensic;
-
-import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
-import static android.Manifest.permission.READ_FORENSIC_STATE;
-
-import android.annotation.EnforcePermission;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PermissionEnforcer;
-import android.os.RemoteException;
-import android.security.forensic.ForensicEvent;
-import android.security.forensic.IForensicService;
-import android.security.forensic.IForensicServiceCommandCallback;
-import android.security.forensic.IForensicServiceStateCallback;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.ServiceThread;
-import com.android.server.SystemService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @hide
- */
-public class ForensicService extends SystemService {
-    private static final String TAG = "ForensicService";
-
-    private static final int MAX_STATE_CALLBACK_NUM = 16;
-    private static final int MSG_ADD_STATE_CALLBACK = 0;
-    private static final int MSG_REMOVE_STATE_CALLBACK = 1;
-    private static final int MSG_ENABLE = 2;
-    private static final int MSG_DISABLE = 3;
-    private static final int MSG_TRANSPORT = 4;
-
-    private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
-    private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
-    private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
-
-    private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
-    private static final int ERROR_PERMISSION_DENIED =
-            IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
-    private static final int ERROR_INVALID_STATE_TRANSITION =
-            IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
-    private static final int ERROR_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
-    private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
-
-    private final Context mContext;
-    private final Handler mHandler;
-    private final ForensicEventTransportConnection mForensicEventTransportConnection;
-    private final DataAggregator mDataAggregator;
-    private final BinderService mBinderService;
-
-    private final ArrayList<IForensicServiceStateCallback> mStateCallbacks = new ArrayList<>();
-    private volatile int mState = STATE_DISABLED;
-
-    public ForensicService(@NonNull Context context) {
-        this(new InjectorImpl(context));
-    }
-
-    @VisibleForTesting
-    ForensicService(@NonNull Injector injector) {
-        super(injector.getContext());
-        mContext = injector.getContext();
-        mHandler = new EventHandler(injector.getLooper(), this);
-        mForensicEventTransportConnection = injector.getForensicEventransportConnection();
-        mDataAggregator = injector.getDataAggregator(this);
-        mBinderService = new BinderService(this, injector.getPermissionEnforcer());
-    }
-
-    @VisibleForTesting
-    protected void setState(int state) {
-        mState = state;
-    }
-
-    private static final class BinderService extends IForensicService.Stub {
-        final ForensicService mService;
-
-        BinderService(ForensicService service, @NonNull PermissionEnforcer permissionEnforcer)  {
-            super(permissionEnforcer);
-            mService = service;
-        }
-
-        @Override
-        @EnforcePermission(READ_FORENSIC_STATE)
-        public void addStateCallback(IForensicServiceStateCallback callback) {
-            addStateCallback_enforcePermission();
-            mService.mHandler.obtainMessage(MSG_ADD_STATE_CALLBACK, callback).sendToTarget();
-        }
-
-        @Override
-        @EnforcePermission(READ_FORENSIC_STATE)
-        public void removeStateCallback(IForensicServiceStateCallback callback) {
-            removeStateCallback_enforcePermission();
-            mService.mHandler.obtainMessage(MSG_REMOVE_STATE_CALLBACK, callback).sendToTarget();
-        }
-
-        @Override
-        @EnforcePermission(MANAGE_FORENSIC_STATE)
-        public void enable(IForensicServiceCommandCallback callback) {
-            enable_enforcePermission();
-            mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget();
-        }
-
-        @Override
-        @EnforcePermission(MANAGE_FORENSIC_STATE)
-        public void disable(IForensicServiceCommandCallback callback) {
-            disable_enforcePermission();
-            mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget();
-        }
-    }
-
-    private static class EventHandler extends Handler {
-        private final ForensicService mService;
-
-        EventHandler(Looper looper, ForensicService service) {
-            super(looper);
-            mService = service;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_ADD_STATE_CALLBACK:
-                    try {
-                        mService.addStateCallback(
-                                (IForensicServiceStateCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_REMOVE_STATE_CALLBACK:
-                    try {
-                        mService.removeStateCallback(
-                                (IForensicServiceStateCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_ENABLE:
-                    try {
-                        mService.enable((IForensicServiceCommandCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_DISABLE:
-                    try {
-                        mService.disable((IForensicServiceCommandCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_TRANSPORT:
-                    mService.transport((List<ForensicEvent>) msg.obj);
-                    break;
-                default:
-                    Slog.w(TAG, "Unknown message: " + msg.what);
-            }
-        }
-    }
-
-    private void addStateCallback(IForensicServiceStateCallback callback) throws RemoteException {
-        for (int i = 0; i < mStateCallbacks.size(); i++) {
-            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
-                return;
-            }
-        }
-        mStateCallbacks.add(callback);
-        callback.onStateChange(mState);
-    }
-
-    private void removeStateCallback(IForensicServiceStateCallback callback)
-            throws RemoteException {
-        for (int i = 0; i < mStateCallbacks.size(); i++) {
-            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
-                mStateCallbacks.remove(i);
-                return;
-            }
-        }
-    }
-
-    private void notifyStateMonitors() {
-        if (mStateCallbacks.size() >= MAX_STATE_CALLBACK_NUM) {
-            mStateCallbacks.removeFirst();
-        }
-
-        for (int i = 0; i < mStateCallbacks.size(); i++) {
-            try {
-                mStateCallbacks.get(i).onStateChange(mState);
-            } catch (RemoteException e) {
-                mStateCallbacks.remove(i);
-            }
-        }
-    }
-
-    private void enable(IForensicServiceCommandCallback callback) throws RemoteException {
-        if (mState == STATE_ENABLED) {
-            callback.onSuccess();
-            return;
-        }
-
-        // TODO: temporarily disable the following for the CTS ForensicManagerTest.
-        //  Enable it when the transport component is ready.
-        // if (!mForensicEventTransportConnection.initialize()) {
-        //     callback.onFailure(ERROR_TRANSPORT_UNAVAILABLE);
-        //   return;
-        // }
-
-        mDataAggregator.enable();
-        mState = STATE_ENABLED;
-        notifyStateMonitors();
-        callback.onSuccess();
-    }
-
-    private void disable(IForensicServiceCommandCallback callback) throws RemoteException {
-        if (mState == STATE_DISABLED) {
-            callback.onSuccess();
-            return;
-        }
-
-        // TODO: temporarily disable the following for the CTS ForensicManagerTest.
-        //  Enable it when the transport component is ready.
-        // mForensicEventTransportConnection.release();
-        mDataAggregator.disable();
-        mState = STATE_DISABLED;
-        notifyStateMonitors();
-        callback.onSuccess();
-    }
-
-    /**
-     * Add a list of ForensicEvent.
-     */
-    public void addNewData(List<ForensicEvent> events) {
-        mHandler.obtainMessage(MSG_TRANSPORT, events).sendToTarget();
-    }
-
-    private void transport(List<ForensicEvent> events) {
-        mForensicEventTransportConnection.addData(events);
-    }
-
-    @Override
-    public void onStart() {
-        try {
-            publishBinderService(Context.FORENSIC_SERVICE, mBinderService);
-        } catch (Throwable t) {
-            Slog.e(TAG, "Could not start the ForensicService.", t);
-        }
-    }
-
-    @VisibleForTesting
-    IForensicService getBinderService() {
-        return mBinderService;
-    }
-
-    interface Injector {
-        Context getContext();
-
-        PermissionEnforcer getPermissionEnforcer();
-
-        Looper getLooper();
-
-        ForensicEventTransportConnection getForensicEventransportConnection();
-
-        DataAggregator getDataAggregator(ForensicService forensicService);
-    }
-
-    private static final class InjectorImpl implements Injector {
-        private final Context mContext;
-
-        InjectorImpl(Context context) {
-            mContext = context;
-        }
-
-        @Override
-        public Context getContext() {
-            return mContext;
-        }
-
-        @Override
-        public PermissionEnforcer getPermissionEnforcer() {
-            return PermissionEnforcer.fromContext(mContext);
-        }
-
-        @Override
-        public Looper getLooper() {
-            ServiceThread serviceThread =
-                    new ServiceThread(
-                            TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
-            serviceThread.start();
-            return serviceThread.getLooper();
-        }
-
-        @Override
-        public ForensicEventTransportConnection getForensicEventransportConnection() {
-            return new ForensicEventTransportConnection(mContext);
-        }
-
-        @Override
-        public DataAggregator getDataAggregator(ForensicService forensicService) {
-            return new DataAggregator(mContext, forensicService);
-        }
-    }
-}
-
diff --git a/services/core/java/com/android/server/security/forensic/OWNERS b/services/core/java/com/android/server/security/forensic/OWNERS
deleted file mode 100644
index 3bc3eb5..0000000
--- a/services/core/java/com/android/server/security/forensic/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-file:platform/frameworks/base:main:/core/java/android/security/forensic/OWNERS
diff --git a/services/core/java/com/android/server/security/forensic/DataAggregator.java b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
similarity index 78%
rename from services/core/java/com/android/server/security/forensic/DataAggregator.java
rename to services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
index cc473ca..06e9dcd 100644
--- a/services/core/java/com/android/server/security/forensic/DataAggregator.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.server.security.forensic;
+package com.android.server.security.intrusiondetection;
 
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.security.forensic.ForensicEvent;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -30,22 +30,22 @@
 import java.util.List;
 
 public class DataAggregator {
-    private static final String TAG = "Forensic DataAggregator";
+    private static final String TAG = "IntrusionDetection DataAggregator";
     private static final int MSG_SINGLE_DATA = 0;
     private static final int MSG_BATCH_DATA = 1;
     private static final int MSG_DISABLE = 2;
 
     private static final int STORED_EVENTS_SIZE_LIMIT = 1024;
-    private final ForensicService mForensicService;
+    private final IntrusionDetectionService mIntrusionDetectionService;
     private final ArrayList<DataSource> mDataSources;
 
     private Context mContext;
-    private List<ForensicEvent> mStoredEvents = new ArrayList<>();
+    private List<IntrusionDetectionEvent> mStoredEvents = new ArrayList<>();
     private ServiceThread mHandlerThread;
     private Handler mHandler;
 
-    public DataAggregator(Context context, ForensicService forensicService) {
-        mForensicService = forensicService;
+    public DataAggregator(Context context, IntrusionDetectionService intrusionDetectionService) {
+        mIntrusionDetectionService = intrusionDetectionService;
         mContext = context;
         mDataSources = new ArrayList<DataSource>();
     }
@@ -83,14 +83,14 @@
     /**
      * DataSource calls it to transmit a single event.
      */
-    public void addSingleData(ForensicEvent event) {
+    public void addSingleData(IntrusionDetectionEvent event) {
         mHandler.obtainMessage(MSG_SINGLE_DATA, event).sendToTarget();
     }
 
     /**
      * DataSource calls it to transmit list of events.
      */
-    public void addBatchData(List<ForensicEvent> events) {
+    public void addBatchData(List<IntrusionDetectionEvent> events) {
         mHandler.obtainMessage(MSG_BATCH_DATA, events).sendToTarget();
     }
 
@@ -104,17 +104,17 @@
         }
     }
 
-    private void onNewSingleData(ForensicEvent event) {
+    private void onNewSingleData(IntrusionDetectionEvent event) {
         if (mStoredEvents.size() < STORED_EVENTS_SIZE_LIMIT) {
             mStoredEvents.add(event);
         } else {
-            mForensicService.addNewData(mStoredEvents);
+            mIntrusionDetectionService.addNewData(mStoredEvents);
             mStoredEvents = new ArrayList<>();
         }
     }
 
-    private void onNewBatchData(List<ForensicEvent> events) {
-        mForensicService.addNewData(events);
+    private void onNewBatchData(List<IntrusionDetectionEvent> events) {
+        mIntrusionDetectionService.addNewData(events);
     }
 
     private void onDisable() {
@@ -135,10 +135,10 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_SINGLE_DATA:
-                    mDataAggregator.onNewSingleData((ForensicEvent) msg.obj);
+                    mDataAggregator.onNewSingleData((IntrusionDetectionEvent) msg.obj);
                     break;
                 case MSG_BATCH_DATA:
-                    mDataAggregator.onNewBatchData((List<ForensicEvent>) msg.obj);
+                    mDataAggregator.onNewBatchData((List<IntrusionDetectionEvent>) msg.obj);
                     break;
                 case MSG_DISABLE:
                     mDataAggregator.onDisable();
diff --git a/services/core/java/com/android/server/security/forensic/DataSource.java b/services/core/java/com/android/server/security/intrusiondetection/DataSource.java
similarity index 93%
rename from services/core/java/com/android/server/security/forensic/DataSource.java
rename to services/core/java/com/android/server/security/intrusiondetection/DataSource.java
index da7ee21..0bc4482 100644
--- a/services/core/java/com/android/server/security/forensic/DataSource.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/DataSource.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.security.forensic;
+package com.android.server.security.intrusiondetection;
 
 public interface DataSource {
     /**
diff --git a/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java b/services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionEventTransportConnection.java
similarity index 74%
rename from services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
rename to services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionEventTransportConnection.java
index b85199e..82f39b3 100644
--- a/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionEventTransportConnection.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.server.security.forensic;
+package com.android.server.security.intrusiondetection;
 
-import static android.Manifest.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE;
+import static android.Manifest.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -27,8 +27,8 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.security.forensic.ForensicEvent;
-import android.security.forensic.IForensicEventTransport;
+import android.security.intrusiondetection.IIntrusionDetectionEventTransport;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -40,20 +40,20 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-public class ForensicEventTransportConnection implements ServiceConnection {
-    private static final String TAG = "ForensicEventTransportConnection";
+public class IntrusionDetectionEventTransportConnection implements ServiceConnection {
+    private static final String TAG = "IntrusionDetectionEventTransportConnection";
     private static final long FUTURE_TIMEOUT_MILLIS = 60 * 1000; // 1 mins
     private final Context mContext;
-    private String mForensicEventTransportConfig;
-    volatile IForensicEventTransport mService;
+    private String mIntrusionDetectionEventTransportConfig;
+    volatile IIntrusionDetectionEventTransport mService;
 
-    public ForensicEventTransportConnection(Context context) {
+    public IntrusionDetectionEventTransportConnection(Context context) {
         mContext = context;
         mService = null;
     }
 
     /**
-     * Initialize the ForensicEventTransport binder service.
+     * Initialize the IntrusionDetectionEventTransport binder service.
      * @return Whether the initialization succeed.
      */
     public boolean initialize() {
@@ -78,11 +78,11 @@
     }
 
     /**
-     * Add data to the ForensicEventTransport binder service.
-     * @param data List of ForensicEvent.
+     * Add data to the IntrusionDetectionEventTransport binder service.
+     * @param data List of IntrusionDetectionEvent.
      * @return Whether the data is added to the binder service.
      */
-    public boolean addData(List<ForensicEvent> data) {
+    public boolean addData(List<IntrusionDetectionEvent> data) {
         AndroidFuture<Integer> resultFuture = new AndroidFuture<>();
         try {
             mService.addData(data, resultFuture);
@@ -119,15 +119,15 @@
     }
 
     private boolean bindService() {
-        mForensicEventTransportConfig = mContext.getString(
-                com.android.internal.R.string.config_forensicEventTransport);
-        if (TextUtils.isEmpty(mForensicEventTransportConfig)) {
-            Slog.e(TAG, "config_forensicEventTransport is empty");
+        mIntrusionDetectionEventTransportConfig = mContext.getString(
+                com.android.internal.R.string.config_intrusionDetectionEventTransport);
+        if (TextUtils.isEmpty(mIntrusionDetectionEventTransportConfig)) {
+            Slog.e(TAG, "config_intrusionDetectionEventTransport is empty");
             return false;
         }
 
         ComponentName serviceComponent =
-                ComponentName.unflattenFromString(mForensicEventTransportConfig);
+                ComponentName.unflattenFromString(mIntrusionDetectionEventTransportConfig);
         if (serviceComponent == null) {
             Slog.e(TAG, "Can't get serviceComponent name");
             return false;
@@ -136,10 +136,10 @@
         try {
             ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(serviceComponent,
                     0 /* flags */);
-            if (!BIND_FORENSIC_EVENT_TRANSPORT_SERVICE.equals(serviceInfo.permission)) {
+            if (!BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE.equals(serviceInfo.permission)) {
                 Slog.e(TAG, serviceComponent.flattenToShortString()
                         + " is not declared with the permission "
-                        + "\"" + BIND_FORENSIC_EVENT_TRANSPORT_SERVICE + "\"");
+                        + "\"" + BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE + "\"");
                 return false;
             }
         } catch (PackageManager.NameNotFoundException e) {
@@ -163,7 +163,7 @@
 
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        mService = IForensicEventTransport.Stub.asInterface(service);
+        mService = IIntrusionDetectionEventTransport.Stub.asInterface(service);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionService.java b/services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionService.java
new file mode 100644
index 0000000..0287b41
--- /dev/null
+++ b/services/core/java/com/android/server/security/intrusiondetection/IntrusionDetectionService.java
@@ -0,0 +1,340 @@
+/*
+ * 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.security.intrusiondetection;
+
+import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE;
+import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE;
+
+import android.annotation.EnforcePermission;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PermissionEnforcer;
+import android.os.RemoteException;
+import android.security.intrusiondetection.IIntrusionDetectionService;
+import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback;
+import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @hide
+ */
+public class IntrusionDetectionService extends SystemService {
+    private static final String TAG = "IntrusionDetectionService";
+
+    private static final int MAX_STATE_CALLBACK_NUM = 16;
+    private static final int MSG_ADD_STATE_CALLBACK = 0;
+    private static final int MSG_REMOVE_STATE_CALLBACK = 1;
+    private static final int MSG_ENABLE = 2;
+    private static final int MSG_DISABLE = 3;
+    private static final int MSG_TRANSPORT = 4;
+
+    private static final int STATE_UNKNOWN =
+            IIntrusionDetectionServiceStateCallback.State.UNKNOWN;
+    private static final int STATE_DISABLED =
+            IIntrusionDetectionServiceStateCallback.State.DISABLED;
+    private static final int STATE_ENABLED =
+            IIntrusionDetectionServiceStateCallback.State.ENABLED;
+
+    private static final int ERROR_UNKNOWN =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.UNKNOWN;
+    private static final int ERROR_PERMISSION_DENIED =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+    private static final int ERROR_INVALID_STATE_TRANSITION =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
+    private static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
+    private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final IntrusionDetectionEventTransportConnection
+            mIntrusionDetectionEventTransportConnection;
+    private final DataAggregator mDataAggregator;
+    private final BinderService mBinderService;
+
+    private final ArrayList<IIntrusionDetectionServiceStateCallback> mStateCallbacks =
+            new ArrayList<>();
+    private volatile int mState = STATE_DISABLED;
+
+    public IntrusionDetectionService(@NonNull Context context) {
+        this(new InjectorImpl(context));
+    }
+
+    @VisibleForTesting
+    IntrusionDetectionService(@NonNull Injector injector) {
+        super(injector.getContext());
+        mContext = injector.getContext();
+        mHandler = new EventHandler(injector.getLooper(), this);
+        mIntrusionDetectionEventTransportConnection =
+                injector.getIntrusionDetectionEventransportConnection();
+        mDataAggregator = injector.getDataAggregator(this);
+        mBinderService = new BinderService(this, injector.getPermissionEnforcer());
+    }
+
+    @VisibleForTesting
+    protected void setState(int state) {
+        mState = state;
+    }
+
+    private static final class BinderService extends IIntrusionDetectionService.Stub {
+        final IntrusionDetectionService mService;
+
+        BinderService(IntrusionDetectionService service,
+                @NonNull PermissionEnforcer permissionEnforcer)  {
+            super(permissionEnforcer);
+            mService = service;
+        }
+
+        @Override
+        @EnforcePermission(READ_INTRUSION_DETECTION_STATE)
+        public void addStateCallback(IIntrusionDetectionServiceStateCallback callback) {
+            addStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_ADD_STATE_CALLBACK, callback).sendToTarget();
+        }
+
+        @Override
+        @EnforcePermission(READ_INTRUSION_DETECTION_STATE)
+        public void removeStateCallback(IIntrusionDetectionServiceStateCallback callback) {
+            removeStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_REMOVE_STATE_CALLBACK, callback).sendToTarget();
+        }
+
+        @Override
+        @EnforcePermission(MANAGE_INTRUSION_DETECTION_STATE)
+        public void enable(IIntrusionDetectionServiceCommandCallback callback) {
+            enable_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget();
+        }
+
+        @Override
+        @EnforcePermission(MANAGE_INTRUSION_DETECTION_STATE)
+        public void disable(IIntrusionDetectionServiceCommandCallback callback) {
+            disable_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget();
+        }
+    }
+
+    private static class EventHandler extends Handler {
+        private final IntrusionDetectionService mService;
+
+        EventHandler(Looper looper, IntrusionDetectionService service) {
+            super(looper);
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_ADD_STATE_CALLBACK:
+                    try {
+                        mService.addStateCallback(
+                                (IIntrusionDetectionServiceStateCallback) msg.obj);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "RemoteException", e);
+                    }
+                    break;
+                case MSG_REMOVE_STATE_CALLBACK:
+                    try {
+                        mService.removeStateCallback(
+                                (IIntrusionDetectionServiceStateCallback) msg.obj);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "RemoteException", e);
+                    }
+                    break;
+                case MSG_ENABLE:
+                    try {
+                        mService.enable((IIntrusionDetectionServiceCommandCallback) msg.obj);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "RemoteException", e);
+                    }
+                    break;
+                case MSG_DISABLE:
+                    try {
+                        mService.disable((IIntrusionDetectionServiceCommandCallback) msg.obj);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "RemoteException", e);
+                    }
+                    break;
+                case MSG_TRANSPORT:
+                    mService.transport((List<IntrusionDetectionEvent>) msg.obj);
+                    break;
+                default:
+                    Slog.w(TAG, "Unknown message: " + msg.what);
+            }
+        }
+    }
+
+    private void addStateCallback(IIntrusionDetectionServiceStateCallback callback)
+            throws RemoteException {
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
+                return;
+            }
+        }
+        mStateCallbacks.add(callback);
+        callback.onStateChange(mState);
+    }
+
+    private void removeStateCallback(IIntrusionDetectionServiceStateCallback callback)
+            throws RemoteException {
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
+                mStateCallbacks.remove(i);
+                return;
+            }
+        }
+    }
+
+    private void notifyStateMonitors() {
+        if (mStateCallbacks.size() >= MAX_STATE_CALLBACK_NUM) {
+            mStateCallbacks.removeFirst();
+        }
+
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            try {
+                mStateCallbacks.get(i).onStateChange(mState);
+            } catch (RemoteException e) {
+                mStateCallbacks.remove(i);
+            }
+        }
+    }
+
+    private void enable(IIntrusionDetectionServiceCommandCallback callback)
+            throws RemoteException {
+        if (mState == STATE_ENABLED) {
+            callback.onSuccess();
+            return;
+        }
+
+        // TODO: temporarily disable the following for the CTS IntrusionDetectionManagerTest.
+        //  Enable it when the transport component is ready.
+        // if (!mIntrusionDetectionEventTransportConnection.initialize()) {
+        //     callback.onFailure(ERROR_TRANSPORT_UNAVAILABLE);
+        //   return;
+        // }
+
+        mDataAggregator.enable();
+        mState = STATE_ENABLED;
+        notifyStateMonitors();
+        callback.onSuccess();
+    }
+
+    private void disable(IIntrusionDetectionServiceCommandCallback callback)
+            throws RemoteException {
+        if (mState == STATE_DISABLED) {
+            callback.onSuccess();
+            return;
+        }
+
+        // TODO: temporarily disable the following for the CTS IntrusionDetectionManagerTest.
+        //  Enable it when the transport component is ready.
+        // mIntrusionDetectionEventTransportConnection.release();
+        mDataAggregator.disable();
+        mState = STATE_DISABLED;
+        notifyStateMonitors();
+        callback.onSuccess();
+    }
+
+    /**
+     * Add a list of IntrusionDetectionEvent.
+     */
+    public void addNewData(List<IntrusionDetectionEvent> events) {
+        mHandler.obtainMessage(MSG_TRANSPORT, events).sendToTarget();
+    }
+
+    private void transport(List<IntrusionDetectionEvent> events) {
+        mIntrusionDetectionEventTransportConnection.addData(events);
+    }
+
+    @Override
+    public void onStart() {
+        try {
+            publishBinderService(Context.INTRUSION_DETECTION_SERVICE, mBinderService);
+        } catch (Throwable t) {
+            Slog.e(TAG, "Could not start the IntrusionDetectionService.", t);
+        }
+    }
+
+    @VisibleForTesting
+    IIntrusionDetectionService getBinderService() {
+        return mBinderService;
+    }
+
+    interface Injector {
+        Context getContext();
+
+        PermissionEnforcer getPermissionEnforcer();
+
+        Looper getLooper();
+
+        IntrusionDetectionEventTransportConnection getIntrusionDetectionEventransportConnection();
+
+        DataAggregator getDataAggregator(IntrusionDetectionService intrusionDetectionService);
+    }
+
+    private static final class InjectorImpl implements Injector {
+        private final Context mContext;
+
+        InjectorImpl(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return PermissionEnforcer.fromContext(mContext);
+        }
+
+        @Override
+        public Looper getLooper() {
+            ServiceThread serviceThread =
+                    new ServiceThread(
+                            TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+            serviceThread.start();
+            return serviceThread.getLooper();
+        }
+
+        @Override
+        public IntrusionDetectionEventTransportConnection
+                getIntrusionDetectionEventransportConnection() {
+            return new IntrusionDetectionEventTransportConnection(mContext);
+        }
+
+        @Override
+        public DataAggregator getDataAggregator(
+                IntrusionDetectionService intrusionDetectionService) {
+            return new DataAggregator(mContext, intrusionDetectionService);
+        }
+    }
+}
+
diff --git a/services/core/java/com/android/server/security/intrusiondetection/OWNERS b/services/core/java/com/android/server/security/intrusiondetection/OWNERS
new file mode 100644
index 0000000..0508067
--- /dev/null
+++ b/services/core/java/com/android/server/security/intrusiondetection/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:main:/core/java/android/security/intrusiondetection/OWNERS
diff --git a/services/core/java/com/android/server/security/forensic/SecurityLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
similarity index 87%
rename from services/core/java/com/android/server/security/forensic/SecurityLogSource.java
rename to services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
index e1b49c4..226f9d8 100644
--- a/services/core/java/com/android/server/security/forensic/SecurityLogSource.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.security.forensic;
+package com.android.server.security.intrusiondetection;
 
 import android.Manifest.permission;
 import android.annotation.RequiresPermission;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.SecurityLog.SecurityEvent;
 import android.content.Context;
-import android.security.forensic.ForensicEvent;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
 
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -31,7 +31,7 @@
 
 public class SecurityLogSource implements DataSource {
 
-    private static final String TAG = "Forensic SecurityLogSource";
+    private static final String TAG = "IntrusionDetection SecurityLogSource";
 
     private SecurityEventCallback mEventCallback = new SecurityEventCallback();
     private DevicePolicyManager mDpm;
@@ -85,12 +85,12 @@
 
         @Override
         public void accept(List<SecurityEvent> events) {
-            List<ForensicEvent> forensicEvents =
+            List<IntrusionDetectionEvent> intrusionDetectionEvents =
                     events.stream()
                             .filter(event -> event != null)
-                            .map(event -> new ForensicEvent(event))
+                            .map(event -> new IntrusionDetectionEvent(event))
                             .collect(Collectors.toList());
-            mDataAggregator.addBatchData(forensicEvents);
+            mDataAggregator.addBatchData(intrusionDetectionEvents);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
new file mode 100644
index 0000000..54ae047
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
@@ -0,0 +1,327 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.BasicPwleSegment;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.MathUtils;
+import android.util.Pair;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Adapts {@link BasicPwleSegment} instances to device-specific {@link PwleSegment}
+ * representations, considering device capabilities such as the supported frequency range
+ * (defined by the intersection points of the frequency-acceleration response curve with the
+ * minimum sensitivity threshold) and the maximum achievable sensitivity level.
+ *
+ * <p>The segments will not be changed if the device doesn't have
+ * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ */
+final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter {
+    private static final String TAG = "BasicToPwleSegmentAdapter";
+    private static final int MIN_REQUIRED_SENSITIVITY_DB_SL = 10;
+    /**
+     * An array of (frequency in Hz, minimum perceptible acceleration in dB) pairs.
+     * Each pair represents the minimum output level (in dB) required for a human to perceive the
+     * vibration at the corresponding frequency.
+     */
+    private static final Pair<Float, Float>[] MIN_PERCEPTIBLE_CURVE = new Pair[]{
+            Pair.create(0.4f, -97.81f), Pair.create(2.0f, -69.86f),
+            Pair.create(3.0f, -62.81f), Pair.create(4.0f, -58.81f),
+            Pair.create(5.0f, -56.69f), Pair.create(6.0f, -54.77f),
+            Pair.create(7.2f, -52.85f), Pair.create(8.0f, -51.77f),
+            Pair.create(8.64f, -50.84f), Pair.create(10.0f, -48.90f),
+            Pair.create(10.37f, -48.52f), Pair.create(12.44f, -46.50f),
+            Pair.create(14.93f, -44.43f), Pair.create(15.0f, -44.35f),
+            Pair.create(17.92f, -41.96f), Pair.create(20.0f, -40.36f),
+            Pair.create(21.5f, -39.60f), Pair.create(25.0f, -37.48f),
+            Pair.create(25.8f, -36.93f), Pair.create(30.0f, -34.31f),
+            Pair.create(35.0f, -33.13f), Pair.create(40.0f, -32.81f),
+            Pair.create(50.0f, -31.94f), Pair.create(60.0f, -31.77f),
+            Pair.create(70.0f, -31.59f), Pair.create(72.0f, -31.55f),
+            Pair.create(80.0f, -31.77f), Pair.create(86.4f, -31.94f),
+            Pair.create(90.0f, -31.73f), Pair.create(100.0f, -31.90f),
+            Pair.create(103.68f, -31.77f), Pair.create(124.42f, -31.70f),
+            Pair.create(149.3f, -31.38f), Pair.create(150.0f, -31.35f),
+            Pair.create(179.16f, -31.02f), Pair.create(200.0f, -30.86f),
+            Pair.create(215.0f, -30.35f), Pair.create(250.0f, -28.98f),
+            Pair.create(258.0f, -28.68f), Pair.create(300.0f, -26.81f),
+            Pair.create(400.0f, -19.81f)
+    };
+    private static final float[] sMinPerceptibleFrequenciesHz =
+            new float[MIN_PERCEPTIBLE_CURVE.length];
+    private static final float[] sMinPerceptibleAccelerationsDb =
+            new float[MIN_PERCEPTIBLE_CURVE.length];
+
+    BasicToPwleSegmentAdapter() {
+
+        // Sort the 'MIN_PERCEPTIBLE_LEVEL' data in ascending order based on the
+        // frequency values (first element of each pair).
+        Arrays.sort(MIN_PERCEPTIBLE_CURVE, Comparator.comparing(pair -> pair.first));
+
+        for (int i = 0; i < MIN_PERCEPTIBLE_CURVE.length; i++) {
+            sMinPerceptibleFrequenciesHz[i] = MIN_PERCEPTIBLE_CURVE[i].first;
+            sMinPerceptibleAccelerationsDb[i] = MIN_PERCEPTIBLE_CURVE[i].second;
+        }
+    }
+
+    @Override
+    public int adaptToVibrator(VibratorInfo info, List<VibrationEffectSegment> segments,
+            int repeatIndex) {
+        if (!Flags.normalizedPwleEffects()
+                || !info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) {
+            // The vibrator does not have PWLE v2 capability, so keep the segments unchanged.
+            return repeatIndex;
+        }
+
+        VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile();
+        float[] frequenciesHz = frequencyProfile.getFrequenciesHz();
+        float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs();
+
+        Pair<Float, Float> frequencyRangeHz = calculateFrequencyRangeHz(
+                Objects.requireNonNull(frequenciesHz), Objects.requireNonNull(accelerationsGs));
+
+        if (frequencyRangeHz == null) {
+            // Failed to retrieve frequency range, so keep the segments unchanged.
+            return repeatIndex;
+        }
+        float minFrequencyHz = frequencyRangeHz.first;
+        float maxFrequencyHz = frequencyRangeHz.second;
+        float maxSensitivityLevel = getMaxSensitivityLevel(frequenciesHz, accelerationsGs,
+                minFrequencyHz, maxFrequencyHz);
+
+        for (int i = 0; i < segments.size(); i++) {
+            VibrationEffectSegment segment = segments.get(i);
+            if (segment instanceof BasicPwleSegment basicPwleSegment) {
+                PwleSegment pwleSegment = convertBasicToPwleSegment(frequencyProfile,
+                        basicPwleSegment, minFrequencyHz, maxFrequencyHz,
+                        maxSensitivityLevel);
+                segments.set(i, pwleSegment);
+            }
+        }
+
+        return repeatIndex;
+    }
+
+    /**
+     * Returns the supported frequency range within which {@link BasicPwleSegment}s are created.
+     * This range, also referred to as the "sharpness range", is defined by the
+     * minimum and maximum frequencies where the actuator's output acceleration exceeds the minimum
+     * required sensitivity level.
+     *
+     * <p>The minimum frequency is the first point where the actuator's frequency to output
+     * acceleration response curve intersects the minimum sensitivity threshold. The maximum
+     * frequency is determined by the second intersection point, or the maximum available
+     * frequency if no second intersection exists.
+     *
+     * @return The supported frequency range, or null if the minimum frequency cannot be determined.
+     */
+    @Nullable
+    private static Pair<Float, Float> calculateFrequencyRangeHz(@NonNull float[] frequenciesHz,
+            @NonNull float[] accelerationsGs) {
+        float minFrequencyHz = Float.NaN;
+        float maxFrequencyHz = Float.NaN;
+
+        for (int i = 0; i < frequenciesHz.length; i++) {
+            float minAcceptableOutputAcceleration = convertSensitivityLevelToAccelerationGs(
+                    MIN_REQUIRED_SENSITIVITY_DB_SL, frequenciesHz[i]);
+
+            if (Float.isNaN(minFrequencyHz)
+                    && minAcceptableOutputAcceleration <= accelerationsGs[i]) {
+                if (i == 0) {
+                    minFrequencyHz = frequenciesHz[0];
+                } else {
+                    minFrequencyHz = MathUtils.constrainedMap(
+                            frequenciesHz[i - 1], frequenciesHz[i],
+                            accelerationsGs[i - 1], accelerationsGs[i],
+                            minAcceptableOutputAcceleration);
+                } // Found the lower bound
+            } else if (!Float.isNaN(minFrequencyHz)
+                    && minAcceptableOutputAcceleration >= accelerationsGs[i]) {
+                maxFrequencyHz = MathUtils.constrainedMap(
+                        frequenciesHz[i - 1], frequenciesHz[i],
+                        accelerationsGs[i - 1], accelerationsGs[i],
+                        minAcceptableOutputAcceleration); // Found the upper bound
+                return new Pair<>(minFrequencyHz, maxFrequencyHz);
+            }
+        }
+
+        if (Float.isNaN(minFrequencyHz)) {
+            // Lower bound was not found
+            Slog.e(TAG,
+                    "Failed to retrieve frequency range. A valid frequency range must be "
+                            + "available to create envelope vibration effects.");
+            return null;
+        }
+
+        // If only the lower bound was found, set the upper bound to the maximum frequency.
+        maxFrequencyHz = frequenciesHz[frequenciesHz.length - 1];
+
+        return new Pair<>(minFrequencyHz, maxFrequencyHz);
+    }
+
+    /**
+     * Converts the {@link BasicPwleSegment} to its equivalent {@link PwleSegment} based on the
+     * devices capabilities.
+     */
+    private static PwleSegment convertBasicToPwleSegment(
+            @NonNull VibratorInfo.FrequencyProfile frequencyProfile,
+            @NonNull BasicPwleSegment basicPwleSegment, float minFrequencyHz, float maxFrequencyHz,
+            float maxSensitivityLevel) {
+
+        float startFrequency = convertSharpnessToFrequencyHz(basicPwleSegment.getStartSharpness(),
+                minFrequencyHz, maxFrequencyHz);
+        float endFrequency = convertSharpnessToFrequencyHz(basicPwleSegment.getEndSharpness(),
+                minFrequencyHz, maxFrequencyHz);
+
+        float startAmplitude = convertIntensityToAmplitude(frequencyProfile,
+                basicPwleSegment.getStartIntensity(), startFrequency, maxSensitivityLevel);
+        float endAmplitude = convertIntensityToAmplitude(frequencyProfile,
+                basicPwleSegment.getEndIntensity(), endFrequency, maxSensitivityLevel);
+
+        return new PwleSegment(startAmplitude, endAmplitude, startFrequency, endFrequency,
+                basicPwleSegment.getDuration());
+    }
+
+    /**
+     * Calculates the amplitude for the vibrator, ranging [0.0, 1.0], based on the desired
+     * intensity and the vibrator's capabilities at the specified frequency.
+     *
+     * <p>This method first converts the desired intensity to an equivalent acceleration value
+     * based on the maximum sensitivity level within the sharpness range. It then compares this
+     * desired acceleration to the maximum acceleration the vibrator can produce at the given
+     * frequency.
+     *
+     * <p>If the desired acceleration exceeds the vibrator's capability, the method returns
+     * 1.0 (maximum amplitude). Otherwise, it returns a normalized amplitude value, calculated as
+     * the ratio of the desired acceleration to the maximum available acceleration at the given
+     * frequency.
+     */
+    private static float convertIntensityToAmplitude(VibratorInfo.FrequencyProfile frequencyProfile,
+            float intensity, float frequencyHz, float maxSensitivityLevel) {
+        if (intensity == 0) {
+            // Zero intensity should map to zero amplitude (i.e. vibrator off)
+            // instead of 0 db SL (i.e. the minimum perceivable output).
+            // This is for consistency with waveform envelopes, to ensure effects
+            // are able to ramp from/to the vibrator off state.
+            return 0;
+        }
+
+        float desiredAcceleration = convertIntensityToAccelerationGs(intensity, frequencyHz,
+                maxSensitivityLevel);
+        float availableAcceleration = frequencyProfile.getOutputAccelerationGs(
+                frequencyHz);
+        return desiredAcceleration >= availableAcceleration ? 1.0f
+                : desiredAcceleration / availableAcceleration;
+    }
+
+    private static float getMaxSensitivityLevel(float[] frequenciesHz, float[] accelerationsGs,
+            float minFrequencyHz, float maxFrequencyHz) {
+        float maxAccelerationGs = Float.MIN_VALUE;
+        int maxAccelerationIndex = -1;
+        for (int i = 0; i < frequenciesHz.length; i++) {
+            float frequency = frequenciesHz[i];
+            if (frequency < minFrequencyHz) {
+                continue;
+            }
+            if (frequency > maxFrequencyHz) {
+                break;
+            }
+            if (accelerationsGs[i] > maxAccelerationGs) {
+                maxAccelerationGs = accelerationsGs[i];
+                maxAccelerationIndex = i;
+            }
+        }
+
+        return convertDecibelToSensitivityLevel(convertAccelerationToDecibel(maxAccelerationGs),
+                frequenciesHz[maxAccelerationIndex]);
+    }
+
+    private static float convertSharpnessToFrequencyHz(float sharpness, float minFrequencyHz,
+            float maxFrequencyHz) {
+        return minFrequencyHz + sharpness * (maxFrequencyHz - minFrequencyHz);
+    }
+
+    private static float convertIntensityToAccelerationGs(float intensity, float frequencyHz,
+            float maxSensitivityLevel) {
+        return convertSensitivityLevelToAccelerationGs(intensity * maxSensitivityLevel,
+                frequencyHz);
+    }
+
+    private static float convertSensitivityLevelToAccelerationGs(float sensitivityLevel,
+            float frequencyHz) {
+        return convertDecibelToAccelerationGs(
+                convertSensitivityLevelToDecibel(sensitivityLevel, frequencyHz));
+    }
+
+    private static float convertDecibelToAccelerationGs(float db) {
+        return (float) Math.pow(10, db / 20);
+    }
+
+    private static float convertSensitivityLevelToDecibel(float sensitivityLevel,
+            float frequencyHz) {
+        float minPerceptibleDbAtFrequency = getMinPerceptibleAccelerationDb(frequencyHz);
+        return sensitivityLevel + minPerceptibleDbAtFrequency;
+    }
+
+    private static float convertAccelerationToDecibel(float accelerationGs) {
+        return (float) (20 * Math.log10(accelerationGs));
+    }
+
+    private static float convertDecibelToSensitivityLevel(float db, float frequencyHz) {
+        float minPerceptibleDbAtFrequency = getMinPerceptibleAccelerationDb(frequencyHz);
+        return db - minPerceptibleDbAtFrequency;
+    }
+
+    /**
+     * Retrieves the minimum perceptible acceleration, in dB, for the specified frequency (hz).
+     */
+    private static float getMinPerceptibleAccelerationDb(float frequencyHz) {
+
+        if (frequencyHz <= sMinPerceptibleFrequenciesHz[0]) {
+            return sMinPerceptibleAccelerationsDb[0];
+        }
+        if (frequencyHz >= sMinPerceptibleFrequenciesHz[sMinPerceptibleFrequenciesHz.length - 1]) {
+            return sMinPerceptibleAccelerationsDb[sMinPerceptibleAccelerationsDb.length - 1];
+        }
+
+        int idx = Arrays.binarySearch(sMinPerceptibleFrequenciesHz, frequencyHz);
+        if (idx >= 0) {
+            return sMinPerceptibleAccelerationsDb[idx];
+        }
+        // This indicates that the value was not found in the list. Adjust index of the
+        // insertion point to be at the lower bound.
+        idx = -idx - 2;
+
+        return MathUtils.constrainedMap(
+                sMinPerceptibleAccelerationsDb[idx],
+                sMinPerceptibleAccelerationsDb[idx + 1],
+                sMinPerceptibleFrequenciesHz[idx], sMinPerceptibleFrequenciesHz[idx + 1],
+                frequencyHz);
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/DeviceAdapter.java b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
index 370f212..e4542b3 100644
--- a/services/core/java/com/android/server/vibrator/DeviceAdapter.java
+++ b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
@@ -67,6 +67,8 @@
                 new SplitSegmentsAdapter(),
                 // Clip amplitudes and frequencies of final segments based on device bandwidth curve
                 new ClippingAmplitudeAndFrequencyAdapter(),
+                // Convert BasicPwleSegments to PwleSegments based on device capabilities
+                new BasicToPwleSegmentAdapter(),
                 // Split Pwle segments based on their duration and device supported limits
                 new SplitPwleSegmentsAdapter()
         );
diff --git a/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
index 87369aa..85ba38d 100644
--- a/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
+++ b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
@@ -18,20 +18,30 @@
 
 import android.hardware.vibrator.IVibrator;
 import android.os.VibratorInfo;
+import android.os.vibrator.BasicPwleSegment;
 import android.os.vibrator.PwleSegment;
 import android.os.vibrator.VibrationEffectSegment;
 
 import java.util.List;
 
 /**
- * Validates Pwle segments to ensure they are compatible with the device's capabilities
- * and adhere to frequency constraints.
+ * Validates {@link PwleSegment} and {@link BasicPwleSegment} instances to ensure they are
+ * compatible with the device's capabilities.
  *
- * <p>The validator verifies that each segment's start and end frequencies fall within
- * the supported range.
- *
- * <p>The segments will be considered invalid of the device does not have
- * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ * <p>This validator performs the following checks:
+ * <ul>
+ *   <li>For {@link PwleSegment}:
+ *     <ul>
+ *       <li>Verifies that the device supports {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ *       <li>Verifies that each segment's start and end frequencies fall within the supported range.
+ *     </ul>
+ *   </li>
+ *   <li>For {@link BasicPwleSegment}:
+ *     <ul>
+ *       <li>Verifies that the device supports {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ *     </ul>
+ *   </li>
+ * </ul>
  */
 final class PwleSegmentsValidator implements VibrationSegmentsValidator {
 
@@ -43,16 +53,17 @@
         float maxFrequency = info.getFrequencyProfile().getMaxFrequencyHz();
 
         for (VibrationEffectSegment segment : segments) {
-            if (!(segment instanceof PwleSegment pwleSegment)) {
-                continue;
-            }
-
-            if (!hasPwleCapability || pwleSegment.getStartFrequencyHz() < minFrequency
-                    || pwleSegment.getStartFrequencyHz() > maxFrequency
-                    || pwleSegment.getEndFrequencyHz() < minFrequency
-                    || pwleSegment.getEndFrequencyHz() > maxFrequency) {
+            if (segment instanceof BasicPwleSegment && !hasPwleCapability) {
                 return false;
             }
+            if (segment instanceof PwleSegment pwleSegment) {
+                if (!hasPwleCapability || pwleSegment.getStartFrequencyHz() < minFrequency
+                        || pwleSegment.getStartFrequencyHz() > maxFrequency
+                        || pwleSegment.getEndFrequencyHz() < minFrequency
+                        || pwleSegment.getEndFrequencyHz() > maxFrequency) {
+                    return false;
+                }
+            }
         }
 
         return true;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index bf93f73..f70dec1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -630,8 +630,8 @@
     // The locusId associated with this activity, if set.
     private LocusId mLocusId;
 
-    // Whether the activity is requesting to limit the system's educational dialogs
-    public boolean mShouldLimitSystemEducationDialogs;
+    // The timestamp of the last request to show the "Open in browser" education
+    public long mRequestOpenInBrowserEducationTimestamp;
 
     // Whether the activity was launched from a bubble.
     private boolean mLaunchedFromBubble;
@@ -7342,9 +7342,8 @@
         return mLocusId;
     }
 
-    void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) {
-        if (mShouldLimitSystemEducationDialogs == limitSystemEducationDialogs) return;
-        mShouldLimitSystemEducationDialogs = limitSystemEducationDialogs;
+    void requestOpenInBrowserEducation() {
+        mRequestOpenInBrowserEducationTimestamp = System.currentTimeMillis();
         final Task task = getTask();
         if (task != null) {
             final boolean force = isVisibleRequested() && this == task.getTopNonFinishingActivity();
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 198e14a..8ff0818 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3915,12 +3915,11 @@
     }
 
     @Override
-    public void setLimitSystemEducationDialogs(
-            IBinder appToken, boolean limitSystemEducationDialogs) {
+    public void requestOpenInBrowserEducation(IBinder appToken) {
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.isInRootTaskLocked(appToken);
             if (r != null) {
-                r.setLimitSystemEducationDialogs(limitSystemEducationDialogs);
+                r.requestOpenInBrowserEducation();
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 4857b02e..70a8f56 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -344,6 +344,11 @@
     private ActivityRecord mTopResumedActivity;
 
     /**
+     * Cached value of the topmost resumed activity that reported to the client.
+     */
+    private ActivityRecord mLastReportedTopResumedActivity;
+
+    /**
      * Flag indicating whether we're currently waiting for the previous top activity to handle the
      * loss of the state and report back before making new activity top resumed.
      */
@@ -2287,15 +2292,13 @@
      * sent to the new top resumed activity.
      */
     ActivityRecord updateTopResumedActivityIfNeeded(String reason) {
-        if (!readyToResume()) {
-            return mTopResumedActivity;
-        }
         final ActivityRecord prevTopActivity = mTopResumedActivity;
         final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
         if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) {
             if (topRootTask == null) {
                 // There's no focused task and there won't have any resumed activity either.
                 scheduleTopResumedActivityStateLossIfNeeded();
+                mTopResumedActivity = null;
             }
             if (mService.isSleepingLocked()) {
                 // There won't be a next resumed activity. The top process should still be updated
@@ -2339,25 +2342,27 @@
 
     /** Schedule current top resumed activity state loss */
     private void scheduleTopResumedActivityStateLossIfNeeded() {
-        if (mTopResumedActivity == null) {
+        if (mLastReportedTopResumedActivity == null) {
             return;
         }
 
         // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity
         // before the prevTopActivity one hasn't reported back yet. So server never sent the top
         // resumed state change message to prevTopActivity.
-        if (!mTopResumedActivityWaitingForPrev
-                && mTopResumedActivity.scheduleTopResumedActivityChanged(false /* onTop */)) {
-            scheduleTopResumedStateLossTimeout(mTopResumedActivity);
+        if (!mTopResumedActivityWaitingForPrev && readyToResume()
+                && mLastReportedTopResumedActivity.scheduleTopResumedActivityChanged(
+                        false /* onTop */)) {
+            scheduleTopResumedStateLossTimeout(mLastReportedTopResumedActivity);
             mTopResumedActivityWaitingForPrev = true;
+            mLastReportedTopResumedActivity = null;
         }
-        mTopResumedActivity = null;
     }
 
     /** Schedule top resumed state change if previous top activity already reported back. */
     private void scheduleTopResumedActivityStateIfNeeded() {
-        if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev) {
+        if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev && readyToResume()) {
             mTopResumedActivity.scheduleTopResumedActivityChanged(true /* onTop */);
+            mLastReportedTopResumedActivity = mTopResumedActivity;
         }
     }
 
@@ -2611,6 +2616,10 @@
      */
     void endDeferResume() {
         mDeferResumeCount--;
+        if (readyToResume() && mLastReportedTopResumedActivity != null
+                && mTopResumedActivity != mLastReportedTopResumedActivity) {
+            scheduleTopResumedActivityStateLossIfNeeded();
+        }
     }
 
     /** @return True if resume can be called. */
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index f2362b9..9754595 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -17,8 +17,8 @@
 package com.android.server.wm;
 
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
 import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
@@ -164,12 +164,12 @@
      * <p>The treatment is enabled when the following conditions are met:
      * <ul>
      * <li>Feature flag gating the camera compatibility free-form treatment is enabled.
-     * <li>Activity isn't opted out by the device manufacturer with override.
+     * <li>Activity is opted in by the device manufacturer with override.
      * </ul>
      */
     boolean shouldApplyFreeformTreatmentForCameraCompat() {
-        return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord,
-                OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
+        return Flags.enableCameraCompatForDesktopWindowing() && isChangeEnabled(mActivityRecord,
+                OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT);
     }
 
     boolean isOverrideOrientationOnlyForCameraEnabled() {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 81a04af..f0e12fe 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -2025,22 +2025,9 @@
         // Fill in some deprecated values.
         rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
         rti.persistentId = rti.taskId;
-        rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
         if (!getTasksAllowed) {
             Task.trimIneffectiveInfo(tr, rti);
         }
-
-        // Fill in organized child task info for the task created by organizer.
-        if (tr.mCreatedByOrganizer) {
-            for (int i = tr.getChildCount() - 1; i >= 0; i--) {
-                final Task childTask = tr.getChildAt(i).asTask();
-                if (childTask != null && childTask.isOrganized()) {
-                    final ActivityManager.RecentTaskInfo cti = new ActivityManager.RecentTaskInfo();
-                    childTask.fillTaskInfo(cti, true /* stripExtras */, tda);
-                    rti.childrenTaskInfos.add(cti);
-                }
-            }
-        }
         return rti;
     }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index dbc3b76c2..87e472a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -128,7 +128,6 @@
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.ActivityManager;
-import android.app.ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData;
 import android.app.ActivityManager.TaskDescription;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
@@ -256,9 +255,6 @@
     private static final String ATTR_MIN_HEIGHT = "min_height";
     private static final String ATTR_PERSIST_TASK_VERSION = "persist_task_version";
     private static final String ATTR_WINDOW_LAYOUT_AFFINITY = "window_layout_affinity";
-    private static final String ATTR_LAST_SNAPSHOT_TASK_SIZE = "last_snapshot_task_size";
-    private static final String ATTR_LAST_SNAPSHOT_CONTENT_INSETS = "last_snapshot_content_insets";
-    private static final String ATTR_LAST_SNAPSHOT_BUFFER_SIZE = "last_snapshot_buffer_size";
 
     // How long to wait for all background Activities to redraw following a call to
     // convertToTranslucent().
@@ -472,10 +468,6 @@
     // NOTE: This value needs to be persisted with each task
     private TaskDescription mTaskDescription;
 
-    // Information about the last snapshot that should be persisted with the task to allow SystemUI
-    // to layout without loading all the task snapshots
-    final PersistedTaskSnapshotData mLastTaskSnapshotData;
-
     /** @see #setCanAffectSystemUiFlags */
     private boolean mCanAffectSystemUiFlags = true;
 
@@ -635,14 +627,13 @@
             ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset,
             boolean _autoRemoveRecents, int _userId, int _effectiveUid,
             String _lastDescription, long lastTimeMoved, boolean neverRelinquishIdentity,
-            TaskDescription _lastTaskDescription, PersistedTaskSnapshotData _lastSnapshotData,
-            int taskAffiliation, int prevTaskId, int nextTaskId, int callingUid,
-            String callingPackage, @Nullable String callingFeatureId, int resizeMode,
-            boolean supportsPictureInPicture, boolean _realActivitySuspended,
-            boolean userSetupComplete, int minWidth, int minHeight, ActivityInfo info,
-            IVoiceInteractionSession _voiceSession, IVoiceInteractor _voiceInteractor,
-            boolean _createdByOrganizer, IBinder _launchCookie, boolean _deferTaskAppear,
-            boolean _removeWithTaskOrganizer) {
+            TaskDescription _lastTaskDescription, int taskAffiliation, int prevTaskId,
+            int nextTaskId, int callingUid, String callingPackage,
+            @Nullable String callingFeatureId, int resizeMode, boolean supportsPictureInPicture,
+            boolean _realActivitySuspended, boolean userSetupComplete, int minWidth, int minHeight,
+            ActivityInfo info, IVoiceInteractionSession _voiceSession,
+            IVoiceInteractor _voiceInteractor, boolean _createdByOrganizer, IBinder _launchCookie,
+            boolean _deferTaskAppear, boolean _removeWithTaskOrganizer) {
         super(atmService, null /* fragmentToken */, _createdByOrganizer, false /* isEmbedded */);
 
         mTaskId = _taskId;
@@ -652,9 +643,6 @@
         mTaskDescription = _lastTaskDescription != null
                 ? _lastTaskDescription
                 : new TaskDescription();
-        mLastTaskSnapshotData = _lastSnapshotData != null
-                ? _lastSnapshotData
-                : new PersistedTaskSnapshotData();
         affinityIntent = _affinityIntent;
         affinity = _affinity;
         rootAffinity = _rootAffinity;
@@ -1212,20 +1200,23 @@
     @Override
     void onResize() {
         super.onResize();
-        updateTaskLayerForFreeform();
+        onTaskBoundsChangedForFreeform();
     }
 
     @Override
     void onMovedByResize() {
         super.onMovedByResize();
-        updateTaskLayerForFreeform();
+        onTaskBoundsChangedForFreeform();
     }
 
-    private void updateTaskLayerForFreeform() {
-        if (!com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()) {
+    private void onTaskBoundsChangedForFreeform() {
+        if (!isVisibleRequested() || !inFreeformWindowingMode()) {
             return;
         }
-        if (!isVisibleRequested() || !inFreeformWindowingMode()) {
+
+        mAtmService.notifyTaskPersisterLocked(this, false /* flush */);
+
+        if (!com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()) {
             return;
         }
         mRootWindowContainer.invalidateTaskLayersAndUpdateOomAdjIfNeeded();
@@ -3111,7 +3102,6 @@
     }
 
     void onSnapshotChanged(TaskSnapshot snapshot) {
-        mLastTaskSnapshotData.set(snapshot);
         mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotChanged(
                 mTaskId, snapshot);
     }
@@ -3423,8 +3413,8 @@
                 ? top.getLastParentBeforePip().mTaskId : INVALID_TASK_ID;
         info.shouldDockBigOverlays = top != null && top.shouldDockBigOverlays;
         info.mTopActivityLocusId = top != null ? top.getLocusId() : null;
-        info.isTopActivityLimitSystemEducationDialogs = top != null
-              ? top.mShouldLimitSystemEducationDialogs : false;
+        info.topActivityRequestOpenInBrowserEducationTimestamp = top != null
+              ? top.mRequestOpenInBrowserEducationTimestamp : 0;
         final Task parentTask = getParent() != null ? getParent().asTask() : null;
         info.parentTaskId = parentTask != null && parentTask.mCreatedByOrganizer
                 ? parentTask.mTaskId
@@ -3523,6 +3513,7 @@
 
         info.capturedLink = null;
         info.capturedLinkTimestamp = 0;
+        info.topActivityRequestOpenInBrowserEducationTimestamp = 0;
     }
 
     @Nullable PictureInPictureParams getPictureInPictureParams() {
@@ -3989,19 +3980,6 @@
         out.attributeInt(null, ATTR_MIN_HEIGHT, mMinHeight);
         out.attributeInt(null, ATTR_PERSIST_TASK_VERSION, PERSIST_TASK_VERSION);
 
-        if (mLastTaskSnapshotData.taskSize != null) {
-            out.attribute(null, ATTR_LAST_SNAPSHOT_TASK_SIZE,
-                    mLastTaskSnapshotData.taskSize.flattenToString());
-        }
-        if (mLastTaskSnapshotData.contentInsets != null) {
-            out.attribute(null, ATTR_LAST_SNAPSHOT_CONTENT_INSETS,
-                    mLastTaskSnapshotData.contentInsets.flattenToString());
-        }
-        if (mLastTaskSnapshotData.bufferSize != null) {
-            out.attribute(null, ATTR_LAST_SNAPSHOT_BUFFER_SIZE,
-                    mLastTaskSnapshotData.bufferSize.flattenToString());
-        }
-
         if (affinityIntent != null) {
             out.startTag(null, TAG_AFFINITYINTENT);
             affinityIntent.saveToXml(out);
@@ -4068,7 +4046,6 @@
         int taskId = INVALID_TASK_ID;
         final int outerDepth = in.getDepth();
         TaskDescription taskDescription = new TaskDescription();
-        PersistedTaskSnapshotData lastSnapshotData = new PersistedTaskSnapshotData();
         int taskAffiliation = INVALID_TASK_ID;
         int prevTaskId = INVALID_TASK_ID;
         int nextTaskId = INVALID_TASK_ID;
@@ -4175,15 +4152,6 @@
                 case ATTR_PERSIST_TASK_VERSION:
                     persistTaskVersion = Integer.parseInt(attrValue);
                     break;
-                case ATTR_LAST_SNAPSHOT_TASK_SIZE:
-                    lastSnapshotData.taskSize = Point.unflattenFromString(attrValue);
-                    break;
-                case ATTR_LAST_SNAPSHOT_CONTENT_INSETS:
-                    lastSnapshotData.contentInsets = Rect.unflattenFromString(attrValue);
-                    break;
-                case ATTR_LAST_SNAPSHOT_BUFFER_SIZE:
-                    lastSnapshotData.bufferSize = Point.unflattenFromString(attrValue);
-                    break;
                 default:
                     if (!attrName.startsWith(TaskDescription.ATTR_TASKDESCRIPTION_PREFIX)) {
                         Slog.w(TAG, "Task: Unknown attribute=" + attrName);
@@ -4277,7 +4245,6 @@
                 .setLastTimeMoved(lastTimeOnTop)
                 .setNeverRelinquishIdentity(neverRelinquishIdentity)
                 .setLastTaskDescription(taskDescription)
-                .setLastSnapshotData(lastSnapshotData)
                 .setTaskAffiliation(taskAffiliation)
                 .setPrevAffiliateTaskId(prevTaskId)
                 .setNextAffiliateTaskId(nextTaskId)
@@ -6443,7 +6410,6 @@
         private long mLastTimeMoved;
         private boolean mNeverRelinquishIdentity;
         private TaskDescription mLastTaskDescription;
-        private PersistedTaskSnapshotData mLastSnapshotData;
         private int mTaskAffiliation;
         private int mPrevAffiliateTaskId = INVALID_TASK_ID;
         private int mNextAffiliateTaskId = INVALID_TASK_ID;
@@ -6671,11 +6637,6 @@
             return this;
         }
 
-        private Builder setLastSnapshotData(PersistedTaskSnapshotData lastSnapshotData) {
-            mLastSnapshotData = lastSnapshotData;
-            return this;
-        }
-
         private Builder setOrigActivity(ComponentName origActivity) {
             mOrigActivity = origActivity;
             return this;
@@ -6824,7 +6785,7 @@
             return new Task(mAtmService, mTaskId, mIntent, mAffinityIntent, mAffinity,
                     mRootAffinity, mRealActivity, mOrigActivity, mRootWasReset, mAutoRemoveRecents,
                     mUserId, mEffectiveUid, mLastDescription, mLastTimeMoved,
-                    mNeverRelinquishIdentity, mLastTaskDescription, mLastSnapshotData,
+                    mNeverRelinquishIdentity, mLastTaskDescription,
                     mTaskAffiliation, mPrevAffiliateTaskId, mNextAffiliateTaskId, mCallingUid,
                     mCallingPackage, mCallingFeatureId, mResizeMode, mSupportsPictureInPicture,
                     mRealActivitySuspended, mUserSetupComplete, mMinWidth, mMinHeight,
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 0918965..c42aa37 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -788,9 +788,7 @@
                 deferResume = false;
                 // Already calls ensureActivityConfig
                 mService.mRootWindowContainer.ensureActivitiesVisible();
-                if (!mService.mRootWindowContainer.resumeFocusedTasksTopActivities()) {
-                    mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT-effects");
-                }
+                mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
             } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
                 for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
                     haveConfigChanges.valueAt(i).forAllActivities(r -> {
@@ -816,10 +814,6 @@
             mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
             if (deferResume) {
                 mService.mTaskSupervisor.endDeferResume();
-                // Transient launching the Recents via HIERARCHY_OP_TYPE_PENDING_INTENT directly
-                // resume the Recents activity with no TRANSACT_EFFECTS_LIFECYCLE. Explicitly
-                // checks if the top resumed activity should be updated after defer-resume ended.
-                mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT");
             }
             mService.continueWindowLayout();
         }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 81af78e..6009848 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -182,7 +182,6 @@
 import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY;
 import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER;
 import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES;
-import static com.android.window.flags.Flags.secureWindowState;
 import static com.android.window.flags.Flags.surfaceTrustedOverlay;
 
 import android.annotation.CallSuper;
@@ -1187,9 +1186,7 @@
         if (surfaceTrustedOverlay() && isWindowTrustedOverlay()) {
             getPendingTransaction().setTrustedOverlay(mSurfaceControl, true);
         }
-        if (secureWindowState()) {
-            getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked());
-        }
+        getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked());
         // All apps should be considered as occluding when computing TrustedPresentation Thresholds.
         final boolean canOccludePresentation = !mSession.mCanAddInternalSystemWindow;
         getPendingTransaction().setCanOccludePresentation(mSurfaceControl, canOccludePresentation);
@@ -6174,18 +6171,10 @@
 
     void setSecureLocked(boolean isSecure) {
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, getName());
-        if (secureWindowState()) {
-            if (mSurfaceControl == null) {
-                return;
-            }
-            getPendingTransaction().setSecure(mSurfaceControl, isSecure);
-        } else {
-            if (mWinAnimator.mSurfaceControl == null) {
-                return;
-            }
-            getPendingTransaction().setSecure(mWinAnimator.mSurfaceControl,
-                    isSecure);
+        if (mSurfaceControl == null) {
+            return;
         }
+        getPendingTransaction().setSecure(mSurfaceControl, isSecure);
         if (mDisplayContent != null) {
             mDisplayContent.refreshImeSecureFlag(getSyncTransaction());
         }
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index a934eea..0154d95 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -49,7 +49,6 @@
 import static com.android.server.wm.WindowStateAnimatorProto.SURFACE;
 import static com.android.server.wm.WindowStateAnimatorProto.SYSTEM_DECOR_RECT;
 import static com.android.server.wm.WindowSurfaceControllerProto.SHOWN;
-import static com.android.window.flags.Flags.secureWindowState;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
 
 import android.content.Context;
@@ -310,12 +309,6 @@
         int flags = SurfaceControl.HIDDEN;
         final WindowManager.LayoutParams attrs = w.mAttrs;
 
-        if (!secureWindowState()) {
-            if (w.isSecureLocked()) {
-                flags |= SurfaceControl.SECURE;
-            }
-        }
-
         if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) {
             flags |= SurfaceControl.SKIP_SCREENSHOT;
         }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index e383375..dece612 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2330,6 +2330,12 @@
     im->getInputManager()->getReader().toggleCapsLockState(deviceId);
 }
 
+static void resetLockedModifierState(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+    im->getInputManager()->getReader().resetLockedModifierState();
+}
+
 static void nativeDisplayRemoved(JNIEnv* env, jobject nativeImplObj, jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
@@ -3134,6 +3140,7 @@
         {"verifyInputEvent", "(Landroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;",
          (void*)nativeVerifyInputEvent},
         {"toggleCapsLock", "(I)V", (void*)nativeToggleCapsLock},
+        {"resetLockedModifierState", "()V", (void*)resetLockedModifierState},
         {"displayRemoved", "(I)V", (void*)nativeDisplayRemoved},
         {"setFocusedApplication", "(ILandroid/view/InputApplicationHandle;)V",
          (void*)nativeSetFocusedApplication},
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9e9dadb..82d49fc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -27,6 +27,7 @@
 import static android.system.OsConstants.O_RDONLY;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.hardware.input.Flags.inputManagerLifecycleSupport;
 import static com.android.server.utils.TimingsTraceAndSlog.SYSTEM_SERVER_TIMING_TAG;
 import static com.android.tradeinmode.flags.Flags.enableTradeInMode;
 
@@ -251,7 +252,8 @@
 import com.android.server.security.KeyChainSystemService;
 import com.android.server.security.advancedprotection.AdvancedProtectionService;
 import com.android.server.security.authenticationpolicy.AuthenticationPolicyService;
-import com.android.server.security.forensic.ForensicService;
+import com.android.server.security.authenticationpolicy.SecureLockDeviceService;
+import com.android.server.security.intrusiondetection.IntrusionDetectionService;
 import com.android.server.security.rkp.RemoteProvisioningService;
 import com.android.server.selinux.SelinuxAuditLogsService;
 import com.android.server.sensorprivacy.SensorPrivacyService;
@@ -1653,7 +1655,12 @@
             t.traceEnd();
 
             t.traceBegin("StartInputManagerService");
-            inputManager = new InputManagerService(context);
+            if (inputManagerLifecycleSupport()) {
+                inputManager = mSystemServiceManager.startService(
+                        InputManagerService.Lifecycle.class).getService();
+            } else {
+                inputManager = new InputManagerService(context);
+            }
             t.traceEnd();
 
             t.traceBegin("DeviceStateManagerService");
@@ -1674,8 +1681,10 @@
             ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
                     DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
                             | DUMP_FLAG_PROTO);
-            ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
-                    /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);
+            if (!inputManagerLifecycleSupport()) {
+                ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
+                        /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);
+            }
             t.traceEnd();
 
             t.traceBegin("SetWindowManagerService");
@@ -1763,8 +1772,8 @@
 
             if (!isWatch && !isTv && !isAutomotive
                     && android.security.Flags.aflApi()) {
-                t.traceBegin("StartForensicService");
-                mSystemServiceManager.startService(ForensicService.class);
+                t.traceBegin("StartIntrusionDetectionService");
+                mSystemServiceManager.startService(IntrusionDetectionService.class);
                 t.traceEnd();
             }
 
@@ -2659,6 +2668,12 @@
             mSystemServiceManager.startService(AuthService.class);
             t.traceEnd();
 
+            if (android.security.Flags.secureLockdown()) {
+                t.traceBegin("StartSecureLockDeviceService.Lifecycle");
+                mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
+                t.traceEnd();
+            }
+
             if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
                 t.traceBegin("StartAuthenticationPolicyService");
                 mSystemServiceManager.startService(AuthenticationPolicyService.class);
@@ -3343,16 +3358,18 @@
                 reportWtf("Notifying NetworkTimeService running", e);
             }
             t.traceEnd();
-            t.traceBegin("MakeInputManagerServiceReady");
-            try {
-                // TODO(BT) Pass parameter to input manager
-                if (inputManagerF != null) {
-                    inputManagerF.systemRunning();
+            if (!inputManagerLifecycleSupport()) {
+                t.traceBegin("MakeInputManagerServiceReady");
+                try {
+                    // TODO(BT) Pass parameter to input manager
+                    if (inputManagerF != null) {
+                        inputManagerF.systemRunning();
+                    }
+                } catch (Throwable e) {
+                    reportWtf("Notifying InputManagerService running", e);
                 }
-            } catch (Throwable e) {
-                reportWtf("Notifying InputManagerService running", e);
+                t.traceEnd();
             }
-            t.traceEnd();
             t.traceBegin("MakeTelephonyRegistryReady");
             try {
                 if (telephonyRegistryF != null) {
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 5bb6b19..d087155 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -194,7 +194,13 @@
                 () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(),
                 true /* expected */,
                 false /* inputViewStarted */);
-        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+            // The IME visibility is only sent at the end of the animation. Therefore, we have to
+            // wait until the visibility was sent to the server and the IME window hidden.
+            eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+        } else {
+            assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        }
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index d66bb00..c6870ad 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -65,6 +65,7 @@
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -103,10 +104,13 @@
 import com.android.server.job.controllers.QuotaController.TimingSession;
 import com.android.server.usage.AppStandbyInternal;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
@@ -135,6 +139,9 @@
     private static final int SOURCE_USER_ID = 0;
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
     private QuotaController mQuotaController;
     private QuotaController.QcConstants mQcConstants;
     private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
@@ -303,7 +310,7 @@
 
     private int getProcessStateQuotaFreeThreshold() {
         synchronized (mQuotaController.mLock) {
-            return mQuotaController.getProcessStateQuotaFreeThreshold();
+            return mQuotaController.getProcessStateQuotaFreeThreshold(mSourceUid);
         }
     }
 
@@ -5197,6 +5204,101 @@
         assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
     }
 
+    @Test
+    @EnableCompatChanges({QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
+            QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS})
+    @RequiresFlagsEnabled({Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS,
+            Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS})
+    public void testTracking_OutOfQuota_ForegroundAndBackground_CompactChangeOverrides() {
+        setDischarging();
+
+        JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1);
+        JobStatus jobTop = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 2);
+        trackJobs(jobBg, jobTop);
+        setStandbyBucket(WORKING_INDEX, jobTop, jobBg); // 2 hour window
+        // Now the package only has 20 seconds to run.
+        final long remainingTimeMs = 20 * SECOND_IN_MILLIS;
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
+                        10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false);
+
+        InOrder inOrder = inOrder(mJobSchedulerService);
+
+        // UID starts out inactive.
+        setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+        // Start the job.
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(jobBg);
+        }
+        advanceElapsedClock(remainingTimeMs / 2);
+        // New job starts after UID is in the foreground. Since the app is now in the foreground, it
+        // should continue to have remainingTimeMs / 2 time remaining.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(jobTop);
+        }
+        advanceElapsedClock(remainingTimeMs);
+
+        // Wait for some extra time to allow for job processing.
+        inOrder.verify(mJobSchedulerService,
+                        timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
+                .onControllerStateChanged(argThat(jobs -> jobs.size() > 0));
+        synchronized (mQuotaController.mLock) {
+            assertEquals(remainingTimeMs / 2,
+                    mQuotaController.getRemainingExecutionTimeLocked(jobBg));
+            assertEquals(remainingTimeMs / 2,
+                    mQuotaController.getRemainingExecutionTimeLocked(jobTop));
+        }
+        // Go to a background state.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+        advanceElapsedClock(remainingTimeMs / 2 + 1);
+        // Only Bg job will be changed from in-quota to out-of-quota.
+        inOrder.verify(mJobSchedulerService,
+                        timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1))
+                .onControllerStateChanged(argThat(jobs -> jobs.size() == 1));
+        // Top job should still be allowed to run.
+        assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+        // New jobs to run.
+        JobStatus jobBg2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 3);
+        JobStatus jobTop2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 4);
+        JobStatus jobFg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 5);
+        setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg);
+
+        advanceElapsedClock(20 * SECOND_IN_MILLIS);
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+                .onControllerStateChanged(argThat(jobs -> jobs.size() == 1));
+        trackJobs(jobFg, jobTop);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(jobTop);
+        }
+        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+        // App still in foreground so everything should be in quota.
+        advanceElapsedClock(20 * SECOND_IN_MILLIS);
+        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+        advanceElapsedClock(20 * SECOND_IN_MILLIS);
+        setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+        inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+                .onControllerStateChanged(argThat(jobs -> jobs.size() == 2));
+        // App is now in background and out of quota. Fg should now change to out of quota since it
+        // wasn't started. Top should remain in quota since it started when the app was in TOP.
+        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        trackJobs(jobBg2);
+        assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+    }
+
     /**
      * Tests that TOP jobs are stopped when an app runs out of quota.
      */
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
index 0d5d277..ed927c6 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
@@ -26,8 +26,7 @@
 import android.os.Process;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.platform.test.ravenwood.RavenwoodConfig;
-import android.platform.test.ravenwood.RavenwoodConfig.Config;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import com.android.internal.os.PowerStats;
 import com.android.server.power.feature.flags.Flags;
@@ -39,10 +38,9 @@
 
 public class WakelockPowerStatsCollectorTest {
 
-    @Config
-    public static final RavenwoodConfig sConfig =
-            new RavenwoodConfig.Builder()
-                    .setProvideMainThread(true)
+    @Rule
+    public final RavenwoodRule mRule =
+            new RavenwoodRule.Builder()
                     .setSystemPropertyImmutable(
                             "persist.sys.com.android.server.power.feature.flags."
                                     + "framework_wakelock_info-override",
diff --git a/services/tests/security/forensic/OWNERS b/services/tests/security/forensic/OWNERS
deleted file mode 100644
index 80c9afb9..0000000
--- a/services/tests/security/forensic/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 36824
-
-file:platform/frameworks/base:main:/core/java/android/security/forensic/OWNERS
diff --git a/services/tests/security/forensic/TEST_MAPPING b/services/tests/security/forensic/TEST_MAPPING
deleted file mode 100644
index bd8b2ab..0000000
--- a/services/tests/security/forensic/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "postsubmit": [
-    {
-      "name": "ForensicServiceTests"
-    }
-  ]
-}
diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
deleted file mode 100644
index 03c449c..0000000
--- a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
+++ /dev/null
@@ -1,400 +0,0 @@
-/*
- * 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.security.forensic;
-
-import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
-import static android.Manifest.permission.READ_FORENSIC_STATE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.SuppressLint;
-import android.app.admin.ConnectEvent;
-import android.app.admin.DnsEvent;
-import android.app.admin.SecurityLog.SecurityEvent;
-import android.content.Context;
-import android.os.Looper;
-import android.os.PermissionEnforcer;
-import android.os.RemoteException;
-import android.os.test.FakePermissionEnforcer;
-import android.os.test.TestLooper;
-import android.security.forensic.ForensicEvent;
-import android.security.forensic.IForensicServiceCommandCallback;
-import android.security.forensic.IForensicServiceStateCallback;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.ServiceThread;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ForensicServiceTest {
-    private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
-    private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
-    private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
-
-    private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
-    private static final int ERROR_PERMISSION_DENIED =
-            IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
-    private static final int ERROR_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
-    private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
-
-    private Context mContext;
-    private ForensicEventTransportConnection mForensicEventTransportConnection;
-    private DataAggregator mDataAggregator;
-    private ForensicService mForensicService;
-    private TestLooper mTestLooper;
-    private Looper mLooper;
-    private TestLooper mTestLooperOfDataAggregator;
-    private Looper mLooperOfDataAggregator;
-    private FakePermissionEnforcer mPermissionEnforcer;
-
-    @SuppressLint("VisibleForTests")
-    @Before
-    public void setUp() {
-        mContext = spy(ApplicationProvider.getApplicationContext());
-
-        mPermissionEnforcer = new FakePermissionEnforcer();
-        mPermissionEnforcer.grant(READ_FORENSIC_STATE);
-        mPermissionEnforcer.grant(MANAGE_FORENSIC_STATE);
-
-        mTestLooper = new TestLooper();
-        mLooper = mTestLooper.getLooper();
-        mTestLooperOfDataAggregator = new TestLooper();
-        mLooperOfDataAggregator = mTestLooperOfDataAggregator.getLooper();
-        mForensicService = new ForensicService(new MockInjector(mContext));
-        mForensicService.onStart();
-    }
-
-    @Test
-    public void testAddStateCallback_NoPermission() {
-        mPermissionEnforcer.revoke(READ_FORENSIC_STATE);
-        StateCallback scb = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb.mState);
-        assertThrows(SecurityException.class,
-                () -> mForensicService.getBinderService().addStateCallback(scb));
-    }
-
-    @Test
-    public void testRemoveStateCallback_NoPermission() {
-        mPermissionEnforcer.revoke(READ_FORENSIC_STATE);
-        StateCallback scb = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb.mState);
-        assertThrows(SecurityException.class,
-                () -> mForensicService.getBinderService().removeStateCallback(scb));
-    }
-
-    @Test
-    public void testEnable_NoPermission() {
-        mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE);
-
-        CommandCallback ccb = new CommandCallback();
-        assertThrows(SecurityException.class,
-                () -> mForensicService.getBinderService().enable(ccb));
-    }
-
-    @Test
-    public void testDisable_NoPermission() {
-        mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE);
-
-        CommandCallback ccb = new CommandCallback();
-        assertThrows(SecurityException.class,
-                () -> mForensicService.getBinderService().disable(ccb));
-    }
-
-    @Test
-    public void testAddStateCallback_Disabled() throws RemoteException {
-        StateCallback scb = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb.mState);
-        mForensicService.getBinderService().addStateCallback(scb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb.mState);
-    }
-
-    @Test
-    public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException {
-        StateCallback scb1 = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb1.mState);
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-
-        StateCallback scb2 = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb2.mState);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb2.mState);
-    }
-
-    @Test
-    public void testRemoveStateCallback() throws RemoteException {
-        mForensicService.setState(STATE_DISABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-
-        doReturn(true).when(mDataAggregator).initialize();
-        doReturn(true).when(mForensicEventTransportConnection).initialize();
-
-        mForensicService.getBinderService().removeStateCallback(scb2);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException {
-        mForensicService.setState(STATE_DISABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-
-        doReturn(true).when(mForensicEventTransportConnection).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-
-        verify(mDataAggregator, times(1)).enable();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testEnable_FromEnabled_TwoStateCallbacks()
-            throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testDisable_FromDisabled_TwoStateCallbacks() throws RemoteException {
-        mForensicService.setState(STATE_DISABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().disable(ccb);
-        mTestLooper.dispatchAll();
-
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testDisable_FromEnabled_TwoStateCallbacks() throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        doNothing().when(mForensicEventTransportConnection).release();
-
-        ServiceThread mockThread = spy(ServiceThread.class);
-        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().disable(ccb);
-        mTestLooper.dispatchAll();
-        mTestLooperOfDataAggregator.dispatchAll();
-        // TODO: We can verify the data sources once we implement them.
-        verify(mockThread, times(1)).quitSafely();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Ignore("Enable once the ForensicEventTransportConnection is ready")
-    @Test
-    public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable()
-            throws RemoteException {
-        mForensicService.setState(STATE_DISABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().addStateCallback(scb1);
-        mForensicService.getBinderService().addStateCallback(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-
-        doReturn(false).when(mForensicEventTransportConnection).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_DISABLED, scb1.mState);
-        assertEquals(STATE_DISABLED, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testDataAggregator_AddBatchData() {
-        mForensicService.setState(STATE_ENABLED);
-        ServiceThread mockThread = spy(ServiceThread.class);
-        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
-
-        SecurityEvent securityEvent = new SecurityEvent(0, new byte[0]);
-        ForensicEvent eventOne = new ForensicEvent(securityEvent);
-
-        ConnectEvent connectEvent = new ConnectEvent("127.0.0.1", 80, null, 0);
-        ForensicEvent eventTwo = new ForensicEvent(connectEvent);
-
-        DnsEvent dnsEvent = new DnsEvent(null, new String[] {"127.0.0.1"}, 1, null, 0);
-        ForensicEvent eventThree = new ForensicEvent(dnsEvent);
-
-        List<ForensicEvent> events = new ArrayList<>();
-        events.add(eventOne);
-        events.add(eventTwo);
-        events.add(eventThree);
-
-        doReturn(true).when(mForensicEventTransportConnection).addData(any());
-
-        mDataAggregator.addBatchData(events);
-        mTestLooperOfDataAggregator.dispatchAll();
-        mTestLooper.dispatchAll();
-
-        ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class);
-        verify(mForensicEventTransportConnection).addData(captor.capture());
-        List<ForensicEvent> receivedEvents = captor.getValue();
-        assertEquals(receivedEvents.size(), 3);
-
-        assertEquals(receivedEvents.get(0).getType(), ForensicEvent.SECURITY_EVENT);
-        assertNotNull(receivedEvents.get(0).getSecurityEvent());
-
-        assertEquals(receivedEvents.get(1).getType(), ForensicEvent.NETWORK_EVENT_CONNECT);
-        assertNotNull(receivedEvents.get(1).getConnectEvent());
-
-        assertEquals(receivedEvents.get(2).getType(), ForensicEvent.NETWORK_EVENT_DNS);
-        assertNotNull(receivedEvents.get(2).getDnsEvent());
-    }
-
-    private class MockInjector implements ForensicService.Injector {
-        private final Context mContext;
-
-        MockInjector(Context context) {
-            mContext = context;
-        }
-
-        @Override
-        public Context getContext() {
-            return mContext;
-        }
-
-        @Override
-        public PermissionEnforcer getPermissionEnforcer() {
-            return mPermissionEnforcer;
-        }
-
-        @Override
-        public Looper getLooper() {
-            return mLooper;
-        }
-
-        @Override
-        public ForensicEventTransportConnection getForensicEventransportConnection() {
-            mForensicEventTransportConnection = spy(new ForensicEventTransportConnection(mContext));
-            return mForensicEventTransportConnection;
-        }
-
-        @Override
-        public DataAggregator getDataAggregator(ForensicService forensicService) {
-            mDataAggregator = spy(new DataAggregator(mContext, forensicService));
-            return mDataAggregator;
-        }
-    }
-
-    private static class StateCallback extends IForensicServiceStateCallback.Stub {
-        int mState = STATE_UNKNOWN;
-
-        @Override
-        public void onStateChange(int state) throws RemoteException {
-            mState = state;
-        }
-    }
-
-    private static class CommandCallback extends IForensicServiceCommandCallback.Stub {
-        Integer mErrorCode = null;
-
-        public void reset() {
-            mErrorCode = null;
-        }
-
-        @Override
-        public void onSuccess() throws RemoteException {
-
-        }
-
-        @Override
-        public void onFailure(int errorCode) throws RemoteException {
-            mErrorCode = errorCode;
-        }
-    }
-}
diff --git a/services/tests/security/forensic/Android.bp b/services/tests/security/intrusiondetection/Android.bp
similarity index 95%
rename from services/tests/security/forensic/Android.bp
rename to services/tests/security/intrusiondetection/Android.bp
index 77a87af..00ac908 100644
--- a/services/tests/security/forensic/Android.bp
+++ b/services/tests/security/intrusiondetection/Android.bp
@@ -9,7 +9,7 @@
 }
 
 android_test {
-    name: "ForensicServiceTests",
+    name: "IntrusionDetectionServiceTests",
     srcs: [
         "src/**/*.java",
     ],
diff --git a/services/tests/security/forensic/AndroidManifest.xml b/services/tests/security/intrusiondetection/AndroidManifest.xml
similarity index 83%
rename from services/tests/security/forensic/AndroidManifest.xml
rename to services/tests/security/intrusiondetection/AndroidManifest.xml
index c5b3d40..f388e7e 100644
--- a/services/tests/security/forensic/AndroidManifest.xml
+++ b/services/tests/security/intrusiondetection/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  package="com.android.server.security.forensic.tests">
+  package="com.android.server.security.intrusiondetection.tests">
 
        <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING"     />
        <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
@@ -25,6 +25,6 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-         android:targetPackage="com.android.server.security.forensic.tests"
-         android:label="Frameworks Forensic Services Tests"/>
+         android:targetPackage="com.android.server.security.intrusiondetection.tests"
+         android:label="Frameworks IntrusionDetection Services Tests"/>
 </manifest>
diff --git a/services/tests/security/forensic/AndroidTest.xml b/services/tests/security/intrusiondetection/AndroidTest.xml
similarity index 82%
rename from services/tests/security/forensic/AndroidTest.xml
rename to services/tests/security/intrusiondetection/AndroidTest.xml
index bbe2e9c..42cb9e3 100644
--- a/services/tests/security/forensic/AndroidTest.xml
+++ b/services/tests/security/intrusiondetection/AndroidTest.xml
@@ -13,19 +13,19 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Runs Frameworks Forensic Service tests.">
+<configuration description="Runs Frameworks IntrusionDetection Service tests.">
     <option name="test-suite-tag" value="apct" />
     <option name="test-suite-tag" value="apct-instrumentation" />
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
-        <option name="test-file-name" value="ForensicServiceTests.apk"/>
+        <option name="test-file-name" value="IntrusionDetectionServiceTests.apk"/>
         <option name="install-arg" value="-t" />
     </target_preparer>
 
-    <option name="test-tag" value="ForensicServiceTests" />
+    <option name="test-tag" value="IntrusionDetectionServiceTests" />
     <test class="com.android.tradefed.testtype.InstrumentationTest" >
-        <option name="package" value="com.android.server.security.forensic.tests" />
+        <option name="package" value="com.android.server.security.intrusiondetection.tests" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
     </test>
diff --git a/services/tests/security/intrusiondetection/OWNERS b/services/tests/security/intrusiondetection/OWNERS
new file mode 100644
index 0000000..2157972
--- /dev/null
+++ b/services/tests/security/intrusiondetection/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36824
+
+file:platform/frameworks/base:main:/core/java/android/security/intrusiondetection/OWNERS
diff --git a/services/tests/security/intrusiondetection/TEST_MAPPING b/services/tests/security/intrusiondetection/TEST_MAPPING
new file mode 100644
index 0000000..24d63e3
--- /dev/null
+++ b/services/tests/security/intrusiondetection/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "IntrusionDetectionServiceTests"
+    }
+  ]
+}
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
new file mode 100644
index 0000000..bc854cf
--- /dev/null
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
@@ -0,0 +1,410 @@
+/*
+ * 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.security.intrusiondetection;
+
+import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE;
+import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.SuppressLint;
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.app.admin.SecurityLog.SecurityEvent;
+import android.content.Context;
+import android.os.Looper;
+import android.os.PermissionEnforcer;
+import android.os.RemoteException;
+import android.os.test.FakePermissionEnforcer;
+import android.os.test.TestLooper;
+import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback;
+import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback;
+import android.security.intrusiondetection.IntrusionDetectionEvent;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.ServiceThread;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class IntrusionDetectionServiceTest {
+    private static final int STATE_UNKNOWN =
+            IIntrusionDetectionServiceStateCallback.State.UNKNOWN;
+    private static final int STATE_DISABLED =
+            IIntrusionDetectionServiceStateCallback.State.DISABLED;
+    private static final int STATE_ENABLED =
+            IIntrusionDetectionServiceStateCallback.State.ENABLED;
+
+    private static final int ERROR_UNKNOWN =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.UNKNOWN;
+    private static final int ERROR_PERMISSION_DENIED =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+    private static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
+    private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
+            IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+
+    private Context mContext;
+    private IntrusionDetectionEventTransportConnection mIntrusionDetectionEventTransportConnection;
+    private DataAggregator mDataAggregator;
+    private IntrusionDetectionService mIntrusionDetectionService;
+    private TestLooper mTestLooper;
+    private Looper mLooper;
+    private TestLooper mTestLooperOfDataAggregator;
+    private Looper mLooperOfDataAggregator;
+    private FakePermissionEnforcer mPermissionEnforcer;
+
+    @SuppressLint("VisibleForTests")
+    @Before
+    public void setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext());
+
+        mPermissionEnforcer = new FakePermissionEnforcer();
+        mPermissionEnforcer.grant(READ_INTRUSION_DETECTION_STATE);
+        mPermissionEnforcer.grant(MANAGE_INTRUSION_DETECTION_STATE);
+
+        mTestLooper = new TestLooper();
+        mLooper = mTestLooper.getLooper();
+        mTestLooperOfDataAggregator = new TestLooper();
+        mLooperOfDataAggregator = mTestLooperOfDataAggregator.getLooper();
+        mIntrusionDetectionService = new IntrusionDetectionService(new MockInjector(mContext));
+        mIntrusionDetectionService.onStart();
+    }
+
+    @Test
+    public void testAddStateCallback_NoPermission() {
+        mPermissionEnforcer.revoke(READ_INTRUSION_DETECTION_STATE);
+        StateCallback scb = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb.mState);
+        assertThrows(SecurityException.class,
+                () -> mIntrusionDetectionService.getBinderService().addStateCallback(scb));
+    }
+
+    @Test
+    public void testRemoveStateCallback_NoPermission() {
+        mPermissionEnforcer.revoke(READ_INTRUSION_DETECTION_STATE);
+        StateCallback scb = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb.mState);
+        assertThrows(SecurityException.class,
+                () -> mIntrusionDetectionService.getBinderService().removeStateCallback(scb));
+    }
+
+    @Test
+    public void testEnable_NoPermission() {
+        mPermissionEnforcer.revoke(MANAGE_INTRUSION_DETECTION_STATE);
+
+        CommandCallback ccb = new CommandCallback();
+        assertThrows(SecurityException.class,
+                () -> mIntrusionDetectionService.getBinderService().enable(ccb));
+    }
+
+    @Test
+    public void testDisable_NoPermission() {
+        mPermissionEnforcer.revoke(MANAGE_INTRUSION_DETECTION_STATE);
+
+        CommandCallback ccb = new CommandCallback();
+        assertThrows(SecurityException.class,
+                () -> mIntrusionDetectionService.getBinderService().disable(ccb));
+    }
+
+    @Test
+    public void testAddStateCallback_Disabled() throws RemoteException {
+        StateCallback scb = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb.mState);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb.mState);
+    }
+
+    @Test
+    public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException {
+        StateCallback scb1 = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb1.mState);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+
+        StateCallback scb2 = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb2.mState);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb2.mState);
+    }
+
+    @Test
+    public void testRemoveStateCallback() throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_DISABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+
+        doReturn(true).when(mDataAggregator).initialize();
+        doReturn(true).when(mIntrusionDetectionEventTransportConnection).initialize();
+
+        mIntrusionDetectionService.getBinderService().removeStateCallback(scb2);
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().enable(ccb);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
+    }
+
+    @Test
+    public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_DISABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+
+        doReturn(true).when(mIntrusionDetectionEventTransportConnection).initialize();
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().enable(ccb);
+        mTestLooper.dispatchAll();
+
+        verify(mDataAggregator, times(1)).enable();
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_ENABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
+    }
+
+    @Test
+    public void testEnable_FromEnabled_TwoStateCallbacks()
+            throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_ENABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_ENABLED, scb2.mState);
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().enable(ccb);
+        mTestLooper.dispatchAll();
+
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_ENABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
+    }
+
+    @Test
+    public void testDisable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_DISABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().disable(ccb);
+        mTestLooper.dispatchAll();
+
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
+    }
+
+    @Test
+    public void testDisable_FromEnabled_TwoStateCallbacks() throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_ENABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_ENABLED, scb2.mState);
+
+        doNothing().when(mIntrusionDetectionEventTransportConnection).release();
+
+        ServiceThread mockThread = spy(ServiceThread.class);
+        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().disable(ccb);
+        mTestLooper.dispatchAll();
+        mTestLooperOfDataAggregator.dispatchAll();
+        // TODO: We can verify the data sources once we implement them.
+        verify(mockThread, times(1)).quitSafely();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
+    }
+
+    @Ignore("Enable once the IntrusionDetectionEventTransportConnection is ready")
+    @Test
+    public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable()
+            throws RemoteException {
+        mIntrusionDetectionService.setState(STATE_DISABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb1);
+        mIntrusionDetectionService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+
+        doReturn(false).when(mIntrusionDetectionEventTransportConnection).initialize();
+
+        CommandCallback ccb = new CommandCallback();
+        mIntrusionDetectionService.getBinderService().enable(ccb);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNotNull(ccb.mErrorCode);
+        assertEquals(ERROR_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue());
+    }
+
+    @Test
+    public void testDataAggregator_AddBatchData() {
+        mIntrusionDetectionService.setState(STATE_ENABLED);
+        ServiceThread mockThread = spy(ServiceThread.class);
+        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
+
+        SecurityEvent securityEvent = new SecurityEvent(0, new byte[0]);
+        IntrusionDetectionEvent eventOne = new IntrusionDetectionEvent(securityEvent);
+
+        ConnectEvent connectEvent = new ConnectEvent(
+                "127.0.0.1", 80, null, 0);
+        IntrusionDetectionEvent eventTwo = new IntrusionDetectionEvent(connectEvent);
+
+        DnsEvent dnsEvent = new DnsEvent(
+                null, new String[] {"127.0.0.1"}, 1, null, 0);
+        IntrusionDetectionEvent eventThree = new IntrusionDetectionEvent(dnsEvent);
+
+        List<IntrusionDetectionEvent> events = new ArrayList<>();
+        events.add(eventOne);
+        events.add(eventTwo);
+        events.add(eventThree);
+
+        doReturn(true).when(mIntrusionDetectionEventTransportConnection).addData(any());
+
+        mDataAggregator.addBatchData(events);
+        mTestLooperOfDataAggregator.dispatchAll();
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<List<IntrusionDetectionEvent>> captor = ArgumentCaptor.forClass(List.class);
+        verify(mIntrusionDetectionEventTransportConnection).addData(captor.capture());
+        List<IntrusionDetectionEvent> receivedEvents = captor.getValue();
+        assertEquals(receivedEvents.size(), 3);
+
+        assertEquals(receivedEvents.get(0).getType(), IntrusionDetectionEvent.SECURITY_EVENT);
+        assertNotNull(receivedEvents.get(0).getSecurityEvent());
+
+        assertEquals(receivedEvents.get(1).getType(),
+                IntrusionDetectionEvent.NETWORK_EVENT_CONNECT);
+        assertNotNull(receivedEvents.get(1).getConnectEvent());
+
+        assertEquals(receivedEvents.get(2).getType(), IntrusionDetectionEvent.NETWORK_EVENT_DNS);
+        assertNotNull(receivedEvents.get(2).getDnsEvent());
+    }
+
+    private class MockInjector implements IntrusionDetectionService.Injector {
+        private final Context mContext;
+
+        MockInjector(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return mPermissionEnforcer;
+        }
+
+        @Override
+        public Looper getLooper() {
+            return mLooper;
+        }
+
+        @Override
+        public IntrusionDetectionEventTransportConnection
+                getIntrusionDetectionEventransportConnection() {
+            mIntrusionDetectionEventTransportConnection =
+                    spy(new IntrusionDetectionEventTransportConnection(mContext));
+            return mIntrusionDetectionEventTransportConnection;
+        }
+
+        @Override
+        public DataAggregator getDataAggregator(
+                IntrusionDetectionService intrusionDetectionService) {
+            mDataAggregator = spy(new DataAggregator(mContext, intrusionDetectionService));
+            return mDataAggregator;
+        }
+    }
+
+    private static class StateCallback extends IIntrusionDetectionServiceStateCallback.Stub {
+        int mState = STATE_UNKNOWN;
+
+        @Override
+        public void onStateChange(int state) throws RemoteException {
+            mState = state;
+        }
+    }
+
+    private static class CommandCallback extends IIntrusionDetectionServiceCommandCallback.Stub {
+        Integer mErrorCode = null;
+
+        public void reset() {
+            mErrorCode = null;
+        }
+
+        @Override
+        public void onSuccess() throws RemoteException {
+
+        }
+
+        @Override
+        public void onFailure(int errorCode) throws RemoteException {
+            mErrorCode = errorCode;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java b/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java
index 2238a1b..ee8eb9b 100644
--- a/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java
@@ -19,12 +19,14 @@
 import static android.adaptiveauth.Flags.FLAG_ENABLE_ADAPTIVE_AUTH;
 import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
 import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
+import static android.security.authenticationpolicy.AuthenticationPolicyManager.ERROR_UNSUPPORTED;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
 import static com.android.server.security.authenticationpolicy.AuthenticationPolicyService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -95,6 +97,8 @@
     private WindowManagerInternal mWindowManager;
     @Mock
     private UserManagerInternal mUserManager;
+    @Mock
+    private SecureLockDeviceServiceInternal mSecureLockDeviceService;
 
     @Captor
     ArgumentCaptor<LockSettingsStateListener> mLockSettingsStateListenerCaptor;
@@ -123,6 +127,11 @@
         LocalServices.addService(WindowManagerInternal.class, mWindowManager);
         LocalServices.removeServiceForTest(UserManagerInternal.class);
         LocalServices.addService(UserManagerInternal.class, mUserManager);
+        if (android.security.Flags.secureLockdown()) {
+            LocalServices.removeServiceForTest(SecureLockDeviceServiceInternal.class);
+            LocalServices.addService(SecureLockDeviceServiceInternal.class,
+                    mSecureLockDeviceService);
+        }
 
         mAuthenticationPolicyService = new AuthenticationPolicyService(
                 mContext, mLockPatternUtils);
@@ -136,6 +145,12 @@
         // Set PRIMARY_USER_ID as the parent of MANAGED_PROFILE_USER_ID
         when(mUserManager.getProfileParentId(eq(MANAGED_PROFILE_USER_ID)))
                 .thenReturn(PRIMARY_USER_ID);
+        if (android.security.Flags.secureLockdown()) {
+            when(mSecureLockDeviceService.enableSecureLockDevice(any()))
+                    .thenReturn(ERROR_UNSUPPORTED);
+            when(mSecureLockDeviceService.disableSecureLockDevice(any()))
+                    .thenReturn(ERROR_UNSUPPORTED);
+        }
     }
 
     @After
@@ -143,6 +158,9 @@
         LocalServices.removeServiceForTest(LockSettingsInternal.class);
         LocalServices.removeServiceForTest(WindowManagerInternal.class);
         LocalServices.removeServiceForTest(UserManagerInternal.class);
+        if (android.security.Flags.secureLockdown()) {
+            LocalServices.removeServiceForTest(SecureLockDeviceServiceInternal.class);
+        }
     }
 
     @Test
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 704c1b8..e6b4bc9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -17178,8 +17178,6 @@
     @Test
     @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);
         // qualifying posted notification
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
@@ -17254,8 +17252,6 @@
     @Test
     @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);
         // qualifying posted notification
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
@@ -17285,8 +17281,6 @@
     @Test
     @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, true);
 
@@ -17350,8 +17344,6 @@
     @Test
     @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, true);
 
@@ -17387,8 +17379,6 @@
     public void testPostPromotableNotification() throws Exception {
         mBinderService.setCanBePromoted(mPkg, mUid, true, true);
         assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isTrue();
-        mContext.getTestablePermissions().setPermission(
-                android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
 
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
@@ -17415,8 +17405,6 @@
     @Test
     @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);
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG"))
@@ -17444,8 +17432,6 @@
     @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testPostPromotableNotification_unimportantNotification() throws Exception {
         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())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG"))
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index 81026fd..d5548a4 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -39,6 +39,7 @@
 import android.os.PersistableBundle;
 import android.os.VibrationEffect;
 import android.os.test.TestLooper;
+import android.os.vibrator.BasicPwleSegment;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
@@ -80,7 +81,12 @@
     private static final int TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS = 20;
     private static final float[] TEST_FREQUENCIES_HZ = new float[]{30f, 50f, 100f, 120f, 150f};
     private static final float[] TEST_OUTPUT_ACCELERATIONS_GS =
-            new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f};
+            new float[]{0.0f, 3.0f, 4.0f, 2.0f, 1.0f};
+
+    private static final float[] TEST_BASIC_FREQUENCIES_HZ = new float[]{50f, 200f, 400f, 500f};
+    private static final float[] TEST_BASIC_OUTPUT_ACCELERATIONS_GS =
+            new float[]{0.05f, 0.5f, 2.0f, 1.0f};
+
     private static final float PWLE_V2_MIN_FREQUENCY = TEST_FREQUENCIES_HZ[0];
     private static final float PWLE_V2_MAX_FREQUENCY =
             TEST_FREQUENCIES_HZ[TEST_FREQUENCIES_HZ.length - 1];
@@ -397,6 +403,46 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegment_withoutPwleV2Capability_returnsNull() {
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.5f, 100),
+                new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+                new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+                new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)),
+                /* repeatIndex= */ 1);
+
+        VibrationEffect.Composed adaptedEffect =
+                (VibrationEffect.Composed) mAdapter.adaptToVibrator(EMPTY_VIBRATOR_ID, effect);
+        assertThat(adaptedEffect).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegment_withPwleV2Capability_returnsAdaptedSegments() {
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new BasicPwleSegment(0.0f, 0.5f, 0.0f, 0.5f, 20),
+                new BasicPwleSegment(0.5f, 1.0f, 0.5f, 1.0f, 100),
+                new BasicPwleSegment(1.0f, 0.0f, 1.0f, 0.5f, 100)),
+                /* repeatIndex= */ 1);
+
+
+        VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList(
+                new PwleSegment(0.0f, 0.16522837f, 63.52442f, 281.7622f, 20),
+                new PwleSegment(0.16522837f, 1.0f, 281.7622f, 500f, 100),
+                new PwleSegment(1.0f, 0.0f, 500, 281.7622f, 100)),
+                /* repeatIndex= */ 1);
+
+        SparseArray<VibratorController> vibrators = new SparseArray<>();
+        vibrators.put(PWLE_V2_VIBRATOR_ID,
+                createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID, TEST_BASIC_FREQUENCIES_HZ,
+                        TEST_BASIC_OUTPUT_ACCELERATIONS_GS));
+        DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+        assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isEqualTo(expected);
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
     public void testPrimitiveWithRelativeDelay_withoutFlag_returnsNull() {
         VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
@@ -477,11 +523,17 @@
     }
 
     private VibratorController createPwleV2VibratorController(int vibratorId) {
+        return createPwleV2VibratorController(vibratorId, TEST_FREQUENCIES_HZ,
+                TEST_OUTPUT_ACCELERATIONS_GS);
+    }
+
+    private VibratorController createPwleV2VibratorController(int vibratorId, float[] frequencies,
+            float[] accelerations) {
         FakeVibratorControllerProvider provider = createVibratorProviderWithEffects(
                 IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
         provider.setResonantFrequency(TEST_RESONANT_FREQUENCY);
-        provider.setFrequenciesHz(TEST_FREQUENCIES_HZ);
-        provider.setOutputAccelerationsGs(TEST_OUTPUT_ACCELERATIONS_GS);
+        provider.setFrequenciesHz(frequencies);
+        provider.setOutputAccelerationsGs(accelerations);
         provider.setMaxEnvelopeEffectSize(TEST_MAX_ENVELOPE_EFFECT_SIZE);
         provider.setMinEnvelopeEffectControlPointDurationMillis(
                 TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS);
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 eb44daa..b4345b6 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -905,10 +905,10 @@
         fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20);
 
         VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
-                .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30)
+                .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 30)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 30)
                 .build();
         HalVibration vibration = startThreadAndDispatcher(effect);
         waitForCompletion();
@@ -930,6 +930,41 @@
 
     @Test
     @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void vibrate_singleVibratorBasicPwle_runsComposePwleV2() {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        fakeVibrator.setResonantFrequency(150);
+        fakeVibrator.setFrequenciesHz(new float[]{50f, 100f, 120f, 150f});
+        fakeVibrator.setOutputAccelerationsGs(new float[]{0.05f, 1.0f, 3.0f, 2.0f});
+        fakeVibrator.setMaxEnvelopeEffectSize(10);
+        fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20);
+
+        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(/*initialSharpness=*/ 1.0f)
+                .addControlPoint(/*intensity=*/ 1.0f, /*sharpness=*/ 1.0f, /*durationMillis=*/ 20)
+                .addControlPoint(/*intensity=*/ 1.0f, /*sharpness=*/ 1.0f, /*durationMillis=*/ 100)
+                .addControlPoint(/*intensity=*/ 0.0f, /*sharpness=*/ 1.0f, /*durationMillis=*/ 100)
+                .build();
+
+        HalVibration vibration = startThreadAndDispatcher(effect);
+        waitForCompletion();
+
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(220L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
+        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+        assertEquals(Arrays.asList(
+                expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 150f, /*timeMillis=*/ 0),
+                expectedPwle(/*amplitude=*/ 1.0f, /*frequencyHz=*/ 150f, /*timeMillis=*/ 20),
+                expectedPwle(/*amplitude=*/ 1.0f, /*frequencyHz=*/ 150f, /*timeMillis=*/ 100),
+                expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 150f, /*timeMillis=*/ 100)
+        ), fakeVibrator.getEffectPwlePoints(vibration.id));
+
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void vibrate_singleVibratorPwle_withInitialFrequency_runsComposePwleV2() {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
@@ -941,10 +976,10 @@
 
         VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
                 .setInitialFrequencyHz(/*initialFrequencyHz=*/ 30)
-                .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30)
-                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20)
-                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30)
+                .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*durationMillis=*/ 30)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*durationMillis=*/ 30)
                 .build();
 
         HalVibration vibration = startThreadAndDispatcher(effect);
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 6f9c890..038e135 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -40,7 +40,7 @@
  */
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES)
+@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_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/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 7f260f8..70f57eb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -357,6 +357,25 @@
         assertEquals(activity1.app, mAtm.mTopApp);
     }
 
+    @Test
+    public void testTopResumedActivity_deferResume() {
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        activity2.setState(ActivityRecord.State.RESUMED, "test");
+        assertEquals(activity2.app, mAtm.mTopApp);
+        reset(activity2);
+
+        // Verify that no top-resumed activity changes to the client while defer-resume enabled.
+        mSupervisor.beginDeferResume();
+        activity1.getTask().moveToFront("test");
+        activity1.setState(ActivityRecord.State.RESUMED, "test");
+        verify(activity2, never()).scheduleTopResumedActivityChanged(eq(false));
+
+        // Verify that the change is scheduled to the client after defer-resumed disabled
+        mSupervisor.endDeferResume();
+        verify(activity2).scheduleTopResumedActivityChanged(eq(false));
+    }
+
     /**
      * We need to launch home again after user unlocked for those displays that do not have
      * encryption aware home app.
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 b91a5b7..d5ed048 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -17,8 +17,8 @@
 package com.android.server.wm;
 
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
 import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
@@ -228,9 +228,8 @@
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
-    public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
+    public void testShouldApplyCameraCompatFreeformTreatment_notEnabledByOverride_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
 
@@ -239,19 +238,9 @@
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
-    public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() {
-        runTestScenario((robot) -> {
-            robot.activity().createActivityWithComponentInNewTask();
-
-            robot.checkShouldApplyFreeformTreatmentForCameraCompat(false);
-        });
-    }
-
-    @Test
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
-    public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
+    public void testShouldApplyCameraCompatFreeformTreatment_overrideAndFlagEnabled_returnsTrue() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
 
@@ -261,8 +250,9 @@
 
     @Test
     @EnableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA,
-            OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void testShouldRecomputeConfigurationForCameraCompat() {
+            OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA,
+            OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testShouldRecomputeConfigurationForFreeformTreatment() {
         runTestScenario((robot) -> {
             robot.conf().enableCameraCompatSplitScreenAspectRatio(true);
             robot.applyOnActivity((a) -> {
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 2635bb3..c427583 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -25,7 +25,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
-import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
@@ -142,8 +142,9 @@
         cameraStateMonitor.startListeningToCameraState();
     }
 
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testFullscreen_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
         doReturn(false).when(mActivity).inFreeformWindowingMode();
@@ -153,23 +154,26 @@
         assertNotInCameraCompatMode();
     }
 
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
 
         assertNotInCameraCompatMode();
     }
 
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         assertNotInCameraCompatMode();
     }
 
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         setDisplayRotation(Surface.ROTATION_0);
@@ -180,6 +184,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         setDisplayRotation(Surface.ROTATION_270);
@@ -190,6 +196,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
         setDisplayRotation(Surface.ROTATION_0);
@@ -200,6 +208,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
         setDisplayRotation(Surface.ROTATION_270);
@@ -209,8 +219,9 @@
         assertActivityRefreshRequested(/* refreshRequested */ false);
     }
 
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         setDisplayRotation(Surface.ROTATION_270);
@@ -229,6 +240,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
@@ -239,31 +252,32 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
-    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
-    public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
+    public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
+                /* checkOrientation */ true));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)
+    public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
         assertTrue(mActivity.info
-                .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT));
-        assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(
-                mActivity, /*  checkOrientation= */ true));
+                .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT));
+        assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
+                /* checkOrientation */ true));
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
-    public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
-        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
-        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-
-        assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(
-                mActivity, /*  checkOrientation= */ true));
-    }
-
-    @Test
-    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
@@ -276,6 +290,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
@@ -291,6 +306,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
@@ -306,6 +322,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -321,6 +338,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
         when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
@@ -335,6 +353,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -349,6 +368,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
         configureActivity(SCREEN_ORIENTATION_FULL_USER);
 
@@ -362,6 +382,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         final float configAspectRatio = 1.5f;
@@ -377,6 +398,7 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         final float configAspectRatio = 1.5f;
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 df17cd1..7ed8283 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -496,24 +496,6 @@
     }
 
     @Test
-    public void testAppendOrganizedChildTaskInfo() {
-        final Task root = createTaskBuilder(".CreatedByOrganizerRoot").build();
-        root.mCreatedByOrganizer = true;
-        // Add organized and non-organized child.
-        final Task child1 = createTaskBuilder(".Task1").setParentTask(root).build();
-        final Task child2 = createTaskBuilder(".Task2").setParentTask(root).build();
-        doReturn(true).when(child1).isOrganized();
-        doReturn(false).when(child2).isOrganized();
-        mRecentTasks.add(root);
-
-        // Make sure only organized child will be appended.
-        final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */);
-        final List<RecentTaskInfo> childrenTaskInfos = infos.get(0).childrenTaskInfos;
-        assertEquals(childrenTaskInfos.size(), 1);
-        assertEquals(childrenTaskInfos.get(0).taskId, child1.mTaskId);
-    }
-
-    @Test
     public void testAddTasksHomeClearUntrackedTasks_expectFinish() {
         // There may be multiple tasks with the same base intent by flags (FLAG_ACTIVITY_NEW_TASK |
         // FLAG_ACTIVITY_MULTIPLE_TASK). If the previous task is still active, it should be removed
@@ -1420,46 +1402,6 @@
     }
 
     @Test
-    public void testLastSnapshotData_snapshotSaved() {
-        final TaskSnapshot snapshot = createSnapshot(new Point(100, 100), new Point(80, 80));
-        final Task task1 = createTaskBuilder(".Task").build();
-        task1.onSnapshotChanged(snapshot);
-
-        mRecentTasks.add(task1);
-        final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */);
-        final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
-                infos.get(0).lastSnapshotData;
-        assertTrue(lastSnapshotData.taskSize.equals(100, 100));
-        assertTrue(lastSnapshotData.bufferSize.equals(80, 80));
-    }
-
-    @Test
-    public void testLastSnapshotData_noBuffer() {
-        final Task task1 = createTaskBuilder(".Task").build();
-        final TaskSnapshot snapshot = createSnapshot(new Point(100, 100), null);
-        task1.onSnapshotChanged(snapshot);
-
-        mRecentTasks.add(task1);
-        final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */);
-        final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
-                infos.get(0).lastSnapshotData;
-        assertTrue(lastSnapshotData.taskSize.equals(100, 100));
-        assertNull(lastSnapshotData.bufferSize);
-    }
-
-    @Test
-    public void testLastSnapshotData_notSet() {
-        final Task task1 = createTaskBuilder(".Task").build();
-
-        mRecentTasks.add(task1);
-        final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */);
-        final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
-                infos.get(0).lastSnapshotData;
-        assertNull(lastSnapshotData.taskSize);
-        assertNull(lastSnapshotData.bufferSize);
-    }
-
-    @Test
     public void testCreateRecentTaskInfo_detachedTask() {
         final Task task = createTaskBuilder(".Task").build();
         final ComponentName componentName = getUniqueComponentName();
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 6490cbe..7cfdec6 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9734,6 +9734,35 @@
             "carrier_supported_satellite_services_per_provider_bundle";
 
     /**
+     * A PersistableBundle that contains a list of key-value pairs, where the values are integer
+     * arrays.
+     * <p>
+     * Keys are the IDs of regional satellite configs as strings and values are
+     * integer arrays of earfcns in the corresponding regions.
+     *
+     * An example config for two regions "1" and "2":
+     * <pre>{@code
+     * <carrier_config>
+     *   <pbundle_as_map name="regional_satellite_earfcn_bundle">
+     *     <int-array name = "1" num = "2">
+     *       <item value = "100"/>
+     *       <item value = "200"/>
+     *     </int-array>
+     *     <int-array name = "2" num = "1">
+     *       <item value = "200"/>
+     *     </int-array>
+     *   </pbundle_as_map>
+     * </carrier_config>
+     * }</pre>
+     * <p>
+     * This config is empty by default.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static final String KEY_REGIONAL_SATELLITE_EARFCN_BUNDLE =
+            "regional_satellite_earfcn_bundle";
+
+    /**
      * This config enables modem to scan satellite PLMNs specified as per
      * {@link #KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE} and attach to same
      * in case cellular networks are not enabled. This will need specific agreement between
@@ -11264,6 +11293,9 @@
         sDefaults.putPersistableBundle(
                 KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE,
                 PersistableBundle.EMPTY);
+        sDefaults.putPersistableBundle(
+                KEY_REGIONAL_SATELLITE_EARFCN_BUNDLE,
+                PersistableBundle.EMPTY);
         sDefaults.putBoolean(KEY_SATELLITE_ATTACH_SUPPORTED_BOOL, false);
         sDefaults.putInt(KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT, 180);
         sDefaults.putIntArray(KEY_NTN_LTE_RSRP_THRESHOLDS_INT_ARRAY,
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java
index 2cd625e..4d495ad 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java
@@ -18,7 +18,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.app.jank.AppJankStats;
 import android.app.jank.Flags;
+import android.app.jank.FrameOverrunHistogram;
 import android.app.jank.JankDataProcessor;
 import android.app.jank.StateTracker;
 import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -39,6 +41,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -154,6 +157,73 @@
         assertEquals(totalFrames, histogramFrames);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void mergeAppJankStats_confirmStatAddedToPendingStats() {
+        HashMap<String, JankDataProcessor.PendingJankStat> pendingStats =
+                mJankDataProcessor.getPendingJankStats();
+
+        assertEquals(pendingStats.size(), 0);
+
+        AppJankStats jankStats = getAppJankStats();
+        mJankDataProcessor.mergeJankStats(jankStats, sActivityName);
+
+        pendingStats = mJankDataProcessor.getPendingJankStats();
+
+        assertEquals(pendingStats.size(), 1);
+    }
+
+    /**
+     * This test confirms matching states are combined into one pending stat.  When JankStats are
+     * merged from outside the platform they will contain widget category, widget id and widget
+     * state. If an incoming JankStats matches a pending stat on all those fields the incoming
+     * JankStat will be merged into the existing stat.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void mergeAppJankStats_confirmStatsWithMatchingStatesAreCombinedIntoOnePendingStat() {
+        AppJankStats jankStats = getAppJankStats();
+        mJankDataProcessor.mergeJankStats(jankStats, sActivityName);
+
+        HashMap<String, JankDataProcessor.PendingJankStat> pendingStats =
+                mJankDataProcessor.getPendingJankStats();
+        assertEquals(pendingStats.size(), 1);
+
+        AppJankStats secondJankStat = getAppJankStats();
+        mJankDataProcessor.mergeJankStats(secondJankStat, sActivityName);
+
+        pendingStats = mJankDataProcessor.getPendingJankStats();
+
+        assertEquals(pendingStats.size(), 1);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void mergeAppJankStats_whenStatsWithMatchingStatesMerge_confirmFrameCountsAdded() {
+        AppJankStats jankStats = getAppJankStats();
+        mJankDataProcessor.mergeJankStats(jankStats, sActivityName);
+        mJankDataProcessor.mergeJankStats(jankStats, sActivityName);
+
+        HashMap<String, JankDataProcessor.PendingJankStat> pendingStats =
+                mJankDataProcessor.getPendingJankStats();
+
+        String statKey = pendingStats.keySet().iterator().next();
+        JankDataProcessor.PendingJankStat pendingStat = pendingStats.get(statKey);
+
+        assertEquals(pendingStats.size(), 1);
+        // The same jankStats objects are merged twice, this should result in the frame counts being
+        // doubled.
+        assertEquals(jankStats.getJankyFrameCount() * 2, pendingStat.getJankyFrames());
+        assertEquals(jankStats.getTotalFrameCount() * 2, pendingStat.getTotalFrames());
+
+        int[] originalHistogramBuckets = jankStats.getFrameOverrunHistogram().getBucketCounters();
+        int[] frameOverrunBuckets = pendingStat.getFrameOverrunBuckets();
+
+        for (int i = 0; i < frameOverrunBuckets.length; i++) {
+            assertEquals(originalHistogramBuckets[i] * 2, frameOverrunBuckets[i]);
+        }
+    }
+
     // TODO b/375005277 add tests that cover logging and releasing resources back to pool.
 
     private long getTotalFramesCounted() {
@@ -276,4 +346,26 @@
         return mockData;
     }
 
+    private AppJankStats getAppJankStats() {
+        AppJankStats jankStats = new AppJankStats(
+                /*App Uid*/APP_ID,
+                /*Widget Id*/"test widget id",
+                /*Widget Category*/AppJankStats.SCROLL,
+                /*Widget State*/AppJankStats.SCROLLING,
+                /*Total Frames*/100,
+                /*Janky Frames*/25,
+                getOverrunHistogram()
+        );
+        return jankStats;
+    }
+
+    private FrameOverrunHistogram getOverrunHistogram() {
+        FrameOverrunHistogram overrunHistogram = new FrameOverrunHistogram();
+        overrunHistogram.addFrameOverrunMillis(-2);
+        overrunHistogram.addFrameOverrunMillis(1);
+        overrunHistogram.addFrameOverrunMillis(5);
+        overrunHistogram.addFrameOverrunMillis(25);
+        return overrunHistogram;
+    }
+
 }
diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml
index d8eb9ff..da510fc 100644
--- a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml
+++ b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml
@@ -18,12 +18,20 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.view.surfacecontroltests">
 
+    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER"/>
+    <uses-permission android:name="android.permission.OBSERVE_PICTURE_PROFILES"/>
+
     <application android:debuggable="true" android:testOnly="true">
         <uses-library android:name="android.test.runner"/>
         <activity
             android:name=".GraphicsActivity"
             android:exported="false">
         </activity>
+        <activity android:name=".SurfaceControlPictureProfileTestActivity"
+                  android:exported="true"
+                  android:turnScreenOn="true"
+                  android:showWhenLocked="true"
+                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml
index 5c0163f..025bf37 100644
--- a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml
+++ b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml
@@ -21,6 +21,9 @@
         <option name="install-arg" value="-t" />
         <option name="test-file-name" value="CtsSurfaceControlTestsStaging.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" >
+        <option name="flag-value" value="media_tv/android.media.tv.flags.apply_picture_profiles=true" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.view.surfacecontroltests" />
         <option name="hidden-api-checks" value="false" />
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java
new file mode 100644
index 0000000..135f710
--- /dev/null
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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 android.view.surfacecontroltests;
+
+import static android.Manifest.permission.OBSERVE_PICTURE_PROFILES;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import static java.util.Arrays.stream;
+
+import android.hardware.HardwareBuffer;
+import android.media.quality.PictureProfileHandle;
+import android.os.Process;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlActivePicture;
+import android.view.SurfaceControlActivePictureListener;
+import android.view.SurfaceView;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.LongStream;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SurfaceControlPictureProfileTest {
+    private static final String TAG = SurfaceControlPictureProfileTest.class.getSimpleName();
+
+    private SurfaceControl[] mSurfaceControls;
+    private SurfaceControl mSurfaceControl;
+
+    @Rule
+    public ActivityTestRule<SurfaceControlPictureProfileTestActivity> mActivityRule =
+            new ActivityTestRule<>(SurfaceControlPictureProfileTestActivity.class);
+
+    @Before
+    public void setup() {
+        SurfaceView[] surfaceViews = mActivityRule.getActivity().getSurfaceViews();
+        mSurfaceControls = new SurfaceControl[surfaceViews.length];
+        // Create a child surface control so we can set a buffer, priority and profile handle all
+        // on one single surface control
+        for (int i = 0; i < mSurfaceControls.length; ++i) {
+            mSurfaceControls[i] = new SurfaceControl.Builder().setName("test").setHidden(false)
+                    .setParent(surfaceViews[i].getSurfaceControl()).build();
+        }
+        mSurfaceControl = mSurfaceControls[0];
+    }
+
+    @Test
+    public void whenPictureProfileApplied_noExecptionsThrown() {
+        assumeTrue("Skipping test because feature flag is disabled",
+                   com.android.graphics.libgui.flags.Flags.applyPictureProfiles());
+        // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead
+        assumeTrue("Skipping test because no picture profile support",
+                   SurfaceControl.getMaxPictureProfiles() > 0);
+
+        // TODO(b/337330263): Load the handle from MediaQualityManager instead
+        PictureProfileHandle handle = new PictureProfileHandle(1);
+        HardwareBuffer buffer = getSolidBuffer(100, 100);
+        new SurfaceControl.Transaction()
+                    .setBuffer(mSurfaceControl, buffer)
+                    .setPictureProfileHandle(mSurfaceControl, handle)
+                    .apply();
+    }
+
+    @Test
+    public void whenStartsListening_callsListener() {
+        assumeTrue("Skipping test because feature flag is disabled",
+                   com.android.graphics.libgui.flags.Flags.applyPictureProfiles());
+        // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead
+        assumeTrue("Skipping test because no picture profile support",
+                   SurfaceControl.getMaxPictureProfiles() > 0);
+
+        BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>();
+        SurfaceControlActivePicture[] pictures;
+        SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() {
+                @Override
+                public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) {
+                    picturesQueue.add(pictures);
+                }
+            };
+        // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead
+        adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES);
+        listener.startListening();
+        {
+            HardwareBuffer buffer = getSolidBuffer(100, 100);
+            new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply();
+        }
+
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        assertThat(pictures).isEmpty();
+    }
+
+    @Test
+    public void whenPictureProfileApplied_callsListenerWithUidAndProfileId() {
+        assumeTrue("Skipping test because feature flag is disabled",
+                   com.android.graphics.libgui.flags.Flags.applyPictureProfiles());
+        // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead
+        assumeTrue("Skipping test because no picture profile support",
+                   SurfaceControl.getMaxPictureProfiles() > 0);
+
+        BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>();
+        SurfaceControlActivePicture[] pictures;
+        SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() {
+                @Override
+                public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) {
+                    picturesQueue.add(pictures);
+                }
+            };
+        // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead
+        adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES);
+        listener.startListening();
+        {
+            HardwareBuffer buffer = getSolidBuffer(100, 100);
+            new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply();
+        }
+
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        assertThat(pictures).isEmpty();
+
+        // TODO(b/337330263): Load the handle from MediaQualityManager instead
+        PictureProfileHandle handle = new PictureProfileHandle(1);
+        HardwareBuffer buffer = getSolidBuffer(100, 100);
+        new SurfaceControl.Transaction()
+                    .setBuffer(mSurfaceControl, buffer)
+                    .setPictureProfileHandle(mSurfaceControl, handle)
+                    .apply();
+
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId()))
+                .containsExactly(handle.getId());
+        assertThat(stream(pictures).map(picture -> picture.getOwnerUid()))
+                .containsExactly(Process.myUid());
+    }
+
+    @Test
+    public void whenPriorityChanges_callsListenerOnlyForLowerPriorityLayers() {
+        assumeTrue("Skipping test because feature flag is disabled",
+                   com.android.graphics.libgui.flags.Flags.applyPictureProfiles());
+        // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead
+        int maxPictureProfiles = SurfaceControl.getMaxPictureProfiles();
+        assumeTrue("Skipping test because no picture profile support", maxPictureProfiles > 0);
+
+        BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>();
+        SurfaceControlActivePicture[] pictures;
+        SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() {
+                @Override
+                public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) {
+                    picturesQueue.add(pictures);
+                }
+            };
+        // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead
+        adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES);
+        listener.startListening();
+        {
+            HardwareBuffer buffer = getSolidBuffer(100, 100);
+            new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply();
+        }
+
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        assertThat(pictures).isEmpty();
+
+        SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        // Use one more picture profile than allowed
+        for (int i = 0; i <= maxPictureProfiles; ++i) {
+            // Increase the number of surface views as necessary to support device configuration.
+            assertThat(i).isLessThan(mSurfaceControls.length);
+
+            // TODO(b/337330263): Load the handle from MediaQualityManager instead
+            PictureProfileHandle handle = new PictureProfileHandle(i + 1);
+            HardwareBuffer buffer = getSolidBuffer(100, 100);
+            transaction
+                    .setBuffer(mSurfaceControls[i], buffer)
+                    .setPictureProfileHandle(mSurfaceControls[i], handle)
+                    .setContentPriority(mSurfaceControls[i], 0);
+        }
+        // Make the first layer low priority (high value)
+        transaction.setContentPriority(mSurfaceControls[0], 2);
+        // Make the last layer higher priority (lower value)
+        transaction.setContentPriority(mSurfaceControls[maxPictureProfiles], 1);
+        transaction.apply();
+
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        assertThat(stream(pictures).map(picture -> picture.getLayerId()))
+                .containsNoDuplicates();
+        // Expect all but the first layer to be listed as an active picture
+        assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId()))
+                .containsExactlyElementsIn(toIterableRange(2, maxPictureProfiles + 1));
+
+        // Change priority and ensure that the first layer gets access
+        new SurfaceControl.Transaction().setContentPriority(mSurfaceControls[0], 0).apply();
+        pictures = pollMs(picturesQueue, 200);
+        assertThat(pictures).isNotNull();
+        // Expect all but the last layer to be listed as an active picture
+        assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId()))
+                .containsExactlyElementsIn(toIterableRange(1, maxPictureProfiles));
+    }
+
+    private static SurfaceControlActivePicture[] pollMs(
+            BlockingQueue<SurfaceControlActivePicture[]> picturesQueue, int waitMs) {
+        SurfaceControlActivePicture[] pictures = null;
+        long nowMs = System.currentTimeMillis();
+        long endTimeMs = nowMs + waitMs;
+        while (nowMs < endTimeMs && pictures == null) {
+            try {
+                pictures = picturesQueue.poll(endTimeMs - nowMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // continue polling until timeout when interrupted
+            }
+            nowMs = System.currentTimeMillis();
+        }
+        return pictures;
+    }
+
+    Iterable<Long> toIterableRange(int start, int stop) {
+        return () -> LongStream.rangeClosed(start, stop).iterator();
+    }
+
+    private void adoptShellPermissionIdentity(String permission) {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(permission);
+    }
+
+    private HardwareBuffer getSolidBuffer(int width, int height) {
+        // We can assume that RGBA_8888 format is supported for every platform.
+        return HardwareBuffer.create(
+                width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_CPU_WRITE_OFTEN);
+    }
+}
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java
new file mode 100644
index 0000000..42fcb26
--- /dev/null
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.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 android.view.surfacecontroltests;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceView;
+
+public class SurfaceControlPictureProfileTestActivity extends Activity {
+    private SurfaceView[] mSurfaceViews;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.picture_profile_test_layout);
+        mSurfaceViews = new SurfaceView[3];
+        mSurfaceViews[0] = (SurfaceView) findViewById(R.id.surfaceview1);
+        mSurfaceViews[1] = (SurfaceView) findViewById(R.id.surfaceview2);
+        mSurfaceViews[2] = (SurfaceView) findViewById(R.id.surfaceview3);
+    }
+
+    public SurfaceView getSurfaceView() {
+        return mSurfaceViews[0];
+    }
+
+    public SurfaceView[] getSurfaceViews() {
+        return mSurfaceViews;
+    }
+}
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml
new file mode 100644
index 0000000..9aa2578
--- /dev/null
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml
@@ -0,0 +1,32 @@
+<?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="match_parent"
+    android:orientation="vertical">
+    <SurfaceView android:id="@+id/surfaceview1"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+    <SurfaceView android:id="@+id/surfaceview2"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+    <SurfaceView android:id="@+id/surfaceview3"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+</LinearLayout>
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
index 01cdbb8..e59b6bd 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
@@ -35,7 +35,7 @@
 /**
  * Test the back and forward transition between 2 activities.
  *
- * To run this test: `atest FlickerTestsAppLaunch:ActivitiesTransitionTest`
+ * To run this test: `atest FlickerTestsAppLaunch:ActivityTransitionTest`
  *
  * Actions:
  * ```
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
index 3d9321c..2bf8cc4 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
@@ -30,7 +30,7 @@
 /**
  * Test cold launching an app from launcher
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenAppColdFromIcon`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromIconColdTest`
  *
  * Actions:
  * ```
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
index 9207530..9c6bf9d 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
@@ -30,7 +30,7 @@
 /**
  * Test launching an app after cold opening camera
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenAppAfterCameraTest`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromIntentColdAfterCameraTest`
  *
  * Notes: Some default assertions are inherited [OpenAppTransition]
  */
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
index cbe7c32..1a53a61 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
@@ -35,7 +35,7 @@
 /**
  * Test cold launching an app from launcher
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenAppColdTest`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromIntentColdTest`
  *
  * Actions:
  * ```
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
index b2941e7..14b6a18 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
@@ -34,7 +34,7 @@
 /**
  * Test warm launching an app from launcher
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenAppWarmTest`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromIntentWarmTest`
  *
  * Actions:
  * ```
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
index 4048e0c..f30fe96 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
@@ -41,7 +41,7 @@
  *
  * This test assumes the device doesn't have AOD enabled
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenAppNonResizeableTest`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromLockscreenViaIntentTest`
  *
  * Actions:
  * ```
diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
index 41423fd..9c552eb 100644
--- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
@@ -41,7 +41,7 @@
 /**
  * Test cold launching camera from launcher by double pressing power button
  *
- * To run this test: `atest FlickerTestsAppLaunch:OpenCameraOnDoubleClickPowerButton`
+ * To run this test: `atest FlickerTestsAppLaunch:OpenCameraFromHomeOnDoubleClickPowerButtonTest`
  *
  * Actions:
  * ```
@@ -140,14 +140,8 @@
 
     @Postsubmit
     @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        flicker.assertLayers {
-            this.visibleLayersShownMoreThanOneConsecutiveEntry(
-                LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS +
-                    listOf(CAMERA_BACKGROUND)
-            )
-        }
-    }
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
     @Postsubmit
     @Test
@@ -170,12 +164,5 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
-
-        private val CAMERA_BACKGROUND =
-            ComponentNameMatcher(
-                "Background for SurfaceView" +
-                    "[com.google.android.GoogleCamera/" +
-                    "com.google.android.apps.camera.legacy.app.activity.main.CameraActivity]"
-            )
     }
 }
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index d2c9eb3..6432827 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -92,7 +92,7 @@
     }
 
     /** Move an app to Desktop by dragging the app handle at the top. */
-    fun enterDesktopModeWithDrag(
+    private fun enterDesktopModeWithDrag(
         wmHelper: WindowManagerStateHelper,
         device: UiDevice,
         motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH)
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt
index 69fde01..9e48848 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt
@@ -65,10 +65,45 @@
             .waitForAndVerify()
     }
 
+    fun startSingleAppMediaProjectionWithExtraIntent(
+        wmHelper: WindowManagerStateHelper,
+        targetApp: StandardAppHelper
+    ) {
+        clickStartMediaProjectionWithExtraIntentButton()
+        chooseSingleAppOption()
+        startScreenSharing()
+        selectTargetApp(targetApp.appName)
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withHomeActivityVisible()
+            .waitForAndVerify()
+    }
+
+    fun startSingleAppMediaProjectionFromRecents(
+        wmHelper: WindowManagerStateHelper,
+        targetApp: StandardAppHelper,
+        recentTasksIndex: Int = 0,
+    ) {
+        clickStartMediaProjectionButton()
+        chooseSingleAppOption()
+        startScreenSharing()
+        selectTargetAppRecent(recentTasksIndex)
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withWindowSurfaceAppeared(targetApp)
+            .waitForAndVerify()
+    }
+
     private fun clickStartMediaProjectionButton() {
         findObject(By.res(packageName, START_MEDIA_PROJECTION_BUTTON_ID)).also { it.click() }
     }
 
+    private fun clickStartMediaProjectionWithExtraIntentButton() {
+        findObject(By.res(packageName, START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID)).also { it.click() }
+    }
+
     private fun chooseEntireScreenOption() {
         findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() }
 
@@ -92,6 +127,13 @@
         findObject(By.text(targetAppName)).also { it.click() }
     }
 
+    private fun selectTargetAppRecent(recentTasksIndex: Int) {
+        // Scroll to to find target app to launch then click app icon it to start capture
+        val recentsTasksRecycler =
+            findObject(By.res(SYSTEMUI_PACKAGE, MEDIA_PROJECTION_RECENT_TASKS))
+        recentsTasksRecycler.children[recentTasksIndex].also{ it.click() }
+    }
+
     private fun chooseSingleAppOption() {
         findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() }
 
@@ -116,8 +158,10 @@
         const val TIMEOUT: Long = 5000L
         const val ACCEPT_RESOURCE_ID: String = "android:id/button1"
         const val START_MEDIA_PROJECTION_BUTTON_ID: String = "button_start_mp"
+        const val START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID: String = "button_start_mp_new_intent"
         val SCREEN_SHARE_OPTIONS_PATTERN: Pattern =
             Pattern.compile("$SYSTEMUI_PACKAGE:id/screen_share_mode_(options|spinner)")
+        const val MEDIA_PROJECTION_RECENT_TASKS: String = "media_projection_recent_tasks_recycler"
         const val ENTIRE_SCREEN_STRING_RES_NAME: String =
             "screen_share_permission_dialog_option_entire_screen"
         const val SINGLE_APP_STRING_RES_NAME: String =
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml
index 46f01e6..c34d200 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml
@@ -16,17 +16,27 @@
 -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
     android:orientation="vertical"
     android:background="@android:color/holo_orange_light">
 
     <Button
         android:id="@+id/button_start_mp"
-        android:layout_width="500dp"
-        android:layout_height="500dp"
+        android:layout_margin="16dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:gravity="center_vertical|center_horizontal"
         android:text="Start Media Projection"
         android:textAppearance="?android:attr/textAppearanceLarge"/>
 
+    <Button
+        android:id="@+id/button_start_mp_new_intent"
+        android:layout_margin="16dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_vertical|center_horizontal"
+        android:text="Start Media Projection with extra intent"
+        android:textAppearance="?android:attr/textAppearanceLarge"/>
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java
index a24a482..b29b874 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java
@@ -19,7 +19,8 @@
 import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.EXTRA_MESSENGER;
 import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_SERVICE_DESTROYED;
 import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_START_FOREGROUND_DONE;
-import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE;
+import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_NORMAL;
+import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_EXTRA_INTENT;
 
 import android.app.Activity;
 import android.content.ComponentName;
@@ -71,13 +72,17 @@
         setContentView(R.layout.activity_start_media_projection);
 
         Button startMediaProjectionButton = findViewById(R.id.button_start_mp);
+        Button startMediaProjectionButton2 = findViewById(R.id.button_start_mp_new_intent);
         startMediaProjectionButton.setOnClickListener(v ->
-                startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE));
+                startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE_NORMAL));
+        startMediaProjectionButton2.setOnClickListener(v ->
+                startActivityForResult(mService.createScreenCaptureIntent(),
+                        REQUEST_CODE_EXTRA_INTENT));
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode != REQUEST_CODE) {
+        if (requestCode != REQUEST_CODE_NORMAL && requestCode != REQUEST_CODE_EXTRA_INTENT) {
             throw new IllegalStateException("Unknown request code: " + requestCode);
         }
         if (resultCode != RESULT_OK) {
@@ -85,6 +90,11 @@
         }
         Log.d(TAG, "onActivityResult");
         startMediaProjectionService(resultCode, data);
+        if (requestCode == REQUEST_CODE_EXTRA_INTENT) {
+            Intent startMain = new Intent(Intent.ACTION_MAIN);
+            startMain.addCategory(Intent.CATEGORY_HOME);
+            startActivity(startMain);
+        }
     }
 
     private void startMediaProjectionService(int resultCode, Intent resultData) {
@@ -122,7 +132,7 @@
                 displayBounds.width(), displayBounds.height(), PixelFormat.RGBA_8888, 1);
 
         mVirtualDisplay = mMediaProjection.createVirtualDisplay(
-                "DanielDisplay",
+                "TestDisplay",
                 displayBounds.width(),
                 displayBounds.height(),
                 DisplayMetrics.DENSITY_HIGH,
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 34350ab..36a89f9 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -1361,7 +1361,7 @@
     @Parameters(method = "systemGesturesTestArguments_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
+        com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_GESTURES
     )
     fun testKeyCombinationGestures(test: TestData) {
         setupKeyGestureController()