Merge "Shift to keycode and modifierState attrs in bookmarks.xml" into main
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig
index d3068d7..a6e9807 100644
--- a/apex/jobscheduler/service/aconfig/alarm.aconfig
+++ b/apex/jobscheduler/service/aconfig/alarm.aconfig
@@ -2,16 +2,6 @@
 container: "system"
 
 flag {
-    name: "use_frozen_state_to_drop_listener_alarms"
-    namespace: "backstage_power"
-    description: "Use frozen state callback to drop listener alarms for cached apps"
-    bug: "324470945"
-    metadata {
-      purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "start_user_before_scheduled_alarms"
     namespace: "multiuser"
     description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 033da2d..60ba3b8 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -282,7 +282,6 @@
 
     private final Injector mInjector;
     int mBroadcastRefCount = 0;
-    boolean mUseFrozenStateToDropListenerAlarms;
     MetricsHelper mMetricsHelper;
     PowerManager.WakeLock mWakeLock;
     SparseIntArray mAlarmsPerUid = new SparseIntArray();
@@ -1784,40 +1783,37 @@
         mMetricsHelper = new MetricsHelper(getContext(), mLock);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
 
-        mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
         mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms()
                 && UserManager.supportsMultipleUsers();
         if (mStartUserBeforeScheduledAlarms) {
             mUserWakeupStore = new UserWakeupStore();
             mUserWakeupStore.init();
         }
-        if (mUseFrozenStateToDropListenerAlarms) {
-            final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
-                final int size = frozenStates.length;
-                if (uids.length != size) {
-                    Slog.wtf(TAG, "Got different length arrays in frozen state callback!"
-                            + " uids.length: " + uids.length + " frozenStates.length: " + size);
-                    // Cannot process received data in any meaningful way.
-                    return;
+        final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
+            final int size = frozenStates.length;
+            if (uids.length != size) {
+                Slog.wtf(TAG, "Got different length arrays in frozen state callback!"
+                        + " uids.length: " + uids.length + " frozenStates.length: " + size);
+                // Cannot process received data in any meaningful way.
+                return;
+            }
+            final IntArray affectedUids = new IntArray();
+            for (int i = 0; i < size; i++) {
+                if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) {
+                    continue;
                 }
-                final IntArray affectedUids = new IntArray();
-                for (int i = 0; i < size; i++) {
-                    if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) {
-                        continue;
-                    }
-                    if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED,
-                            uids[i])) {
-                        continue;
-                    }
-                    affectedUids.add(uids[i]);
+                if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED,
+                        uids[i])) {
+                    continue;
                 }
-                if (affectedUids.size() > 0) {
-                    removeExactListenerAlarms(affectedUids.toArray());
-                }
-            };
-            final ActivityManager am = getContext().getSystemService(ActivityManager.class);
-            am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback);
-        }
+                affectedUids.add(uids[i]);
+            }
+            if (affectedUids.size() > 0) {
+                removeExactListenerAlarms(affectedUids.toArray());
+            }
+        };
+        final ActivityManager am = getContext().getSystemService(ActivityManager.class);
+        am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback);
 
         mListenerDeathRecipient = new IBinder.DeathRecipient() {
             @Override
@@ -2994,13 +2990,10 @@
 
             pw.println("Feature Flags:");
             pw.increaseIndent();
-            pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
-                    mUseFrozenStateToDropListenerAlarms);
-            pw.println();
             pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
                     Flags.startUserBeforeScheduledAlarms());
-            pw.decreaseIndent();
             pw.println();
+            pw.decreaseIndent();
             pw.println();
 
             pw.println("App Standby Parole: " + mAppStandbyParole);
@@ -5146,38 +5139,6 @@
                 removeForStoppedLocked(uid);
             }
         }
-
-        @Override
-        public void handleUidCachedChanged(int uid, boolean cached) {
-            if (mUseFrozenStateToDropListenerAlarms) {
-                // Use ActivityManager#UidFrozenStateChangedCallback instead.
-                return;
-            }
-            if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) {
-                return;
-            }
-            // Apps can quickly get frozen after being cached, breaking the exactness guarantee on
-            // listener alarms. So going forward, the contract of exact listener alarms explicitly
-            // states that they will be removed as soon as the app goes out of lifecycle. We still
-            // allow a short grace period for quick shuffling of proc-states that may happen
-            // unexpectedly when switching between different lifecycles and is generally hard for
-            // apps to avoid.
-
-            final long delay;
-            synchronized (mLock) {
-                delay = mConstants.CACHED_LISTENER_REMOVAL_DELAY;
-            }
-            final Integer uidObj = uid;
-
-            if (cached && !mHandler.hasEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED,
-                    uidObj)) {
-                mHandler.sendMessageDelayed(
-                        mHandler.obtainMessage(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj),
-                        delay);
-            } else {
-                mHandler.removeEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj);
-            }
-        }
     };
 
     private final BroadcastStats getStatsLocked(PendingIntent pi) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 06cf9a5..b386280 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -23,6 +23,7 @@
     field public static final String ACTIVITY_RECOGNITION = "android.permission.ACTIVITY_RECOGNITION";
     field public static final String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL";
     field public static final String ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS";
+    field @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public static final String APPLY_PICTURE_PROFILE = "android.permission.APPLY_PICTURE_PROFILE";
     field public static final String BATTERY_STATS = "android.permission.BATTERY_STATS";
     field public static final String BIND_ACCESSIBILITY_SERVICE = "android.permission.BIND_ACCESSIBILITY_SERVICE";
     field public static final String BIND_APPWIDGET = "android.permission.BIND_APPWIDGET";
@@ -336,7 +337,7 @@
     field public static final String WRITE_SECURE_SETTINGS = "android.permission.WRITE_SECURE_SETTINGS";
     field public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
     field public static final String WRITE_SYNC_SETTINGS = "android.permission.WRITE_SYNC_SETTINGS";
-    field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES";
+    field @FlaggedApi("com.android.settingslib.flags.write_system_preference_permission_enabled") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES";
     field public static final String WRITE_VOICEMAIL = "com.android.voicemail.permission.WRITE_VOICEMAIL";
   }
 
@@ -1276,6 +1277,7 @@
     field public static final int paddingStart = 16843699; // 0x10103b3
     field public static final int paddingTop = 16842967; // 0x10100d7
     field public static final int paddingVertical = 16844094; // 0x101053e
+    field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat;
     field public static final int panelBackground = 16842846; // 0x101005e
     field public static final int panelColorBackground = 16842849; // 0x1010061
     field public static final int panelColorForeground = 16842848; // 0x1010060
@@ -17496,6 +17498,25 @@
     method public void setIntUniform(@NonNull String, @NonNull int[]);
   }
 
+  @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode {
+    ctor public RuntimeXfermode(@NonNull String);
+    method public void setColorUniform(@NonNull String, @ColorInt int);
+    method public void setColorUniform(@NonNull String, @ColorLong long);
+    method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color);
+    method public void setFloatUniform(@NonNull String, float);
+    method public void setFloatUniform(@NonNull String, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float, float);
+    method public void setFloatUniform(@NonNull String, @NonNull float[]);
+    method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter);
+    method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader);
+    method public void setIntUniform(@NonNull String, int);
+    method public void setIntUniform(@NonNull String, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int, int);
+    method public void setIntUniform(@NonNull String, @NonNull int[]);
+  }
+
   public class Shader {
     ctor @Deprecated public Shader();
     method public boolean getLocalMatrix(@NonNull android.graphics.Matrix);
@@ -18826,6 +18847,19 @@
     field public static final int TRANSFER_UNSPECIFIED = 0; // 0x0
   }
 
+  @FlaggedApi("android.hardware.flags.luts_api") public final class DisplayLuts {
+    ctor @FlaggedApi("android.hardware.flags.luts_api") public DisplayLuts();
+    method @FlaggedApi("android.hardware.flags.luts_api") public void set(@NonNull android.hardware.DisplayLuts.Entry);
+    method @FlaggedApi("android.hardware.flags.luts_api") public void set(@NonNull android.hardware.DisplayLuts.Entry, @NonNull android.hardware.DisplayLuts.Entry);
+  }
+
+  @FlaggedApi("android.hardware.flags.luts_api") public static class DisplayLuts.Entry {
+    ctor @FlaggedApi("android.hardware.flags.luts_api") public DisplayLuts.Entry(@NonNull float[], int, int);
+    method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public float[] getBuffer();
+    method @FlaggedApi("android.hardware.flags.luts_api") public int getDimension();
+    method @FlaggedApi("android.hardware.flags.luts_api") public int getSamplingKey();
+  }
+
   public class GeomagneticField {
     ctor public GeomagneticField(float, float, float, long);
     method public float getDeclination();
@@ -18887,8 +18921,19 @@
     field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c
   }
 
+  @FlaggedApi("android.hardware.flags.luts_api") public final class LutProperties {
+    method @FlaggedApi("android.hardware.flags.luts_api") public int getDimension();
+    method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public int[] getSamplingKeys();
+    method @FlaggedApi("android.hardware.flags.luts_api") public int getSize();
+    field @FlaggedApi("android.hardware.flags.luts_api") public static final int ONE_DIMENSION = 1; // 0x1
+    field @FlaggedApi("android.hardware.flags.luts_api") public static final int SAMPLING_KEY_MAX_RGB = 1; // 0x1
+    field @FlaggedApi("android.hardware.flags.luts_api") public static final int SAMPLING_KEY_RGB = 0; // 0x0
+    field @FlaggedApi("android.hardware.flags.luts_api") public static final int THREE_DIMENSION = 3; // 0x3
+  }
+
   @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public final class OverlayProperties implements android.os.Parcelable {
     method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public int describeContents();
+    method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public android.hardware.LutProperties[] getLutProperties();
     method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public boolean isCombinationSupported(int, int);
     method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public boolean isMixedColorSpacesSupported();
     method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -21982,7 +22027,7 @@
   public final class AudioPlaybackConfiguration implements android.os.Parcelable {
     method public int describeContents();
     method public android.media.AudioAttributes getAudioAttributes();
-    method @Nullable public android.media.AudioDeviceInfo getAudioDeviceInfo();
+    method @Deprecated @FlaggedApi("android.media.audio.routed_device_ids") @Nullable public android.media.AudioDeviceInfo getAudioDeviceInfo();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioPlaybackConfiguration> CREATOR;
   }
@@ -22065,6 +22110,7 @@
     method public android.media.AudioDeviceInfo getPreferredDevice();
     method public int getRecordingState();
     method public android.media.AudioDeviceInfo getRoutedDevice();
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices();
     method public int getSampleRate();
     method public int getState();
     method public int getTimestamp(@NonNull android.media.AudioTimestamp, int);
@@ -22159,6 +22205,7 @@
     method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public android.media.AudioDeviceInfo getPreferredDevice();
     method public android.media.AudioDeviceInfo getRoutedDevice();
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public default java.util.List<android.media.AudioDeviceInfo> getRoutedDevices();
     method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener);
     method public boolean setPreferredDevice(android.media.AudioDeviceInfo);
   }
@@ -22217,6 +22264,7 @@
     method public int getPositionNotificationPeriod();
     method public android.media.AudioDeviceInfo getPreferredDevice();
     method public android.media.AudioDeviceInfo getRoutedDevice();
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices();
     method public int getSampleRate();
     method @IntRange(from=1) public int getStartThresholdInFrames();
     method public int getState();
@@ -24385,6 +24433,7 @@
     method @NonNull public android.media.PlaybackParams getPlaybackParams();
     method public android.media.AudioDeviceInfo getPreferredDevice();
     method public android.media.AudioDeviceInfo getRoutedDevice();
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices();
     method public int getSelectedTrack(int) throws java.lang.IllegalStateException;
     method @NonNull public android.media.SyncParams getSyncParams();
     method @Nullable public android.media.MediaTimestamp getTimestamp();
@@ -24598,6 +24647,7 @@
     method public android.os.PersistableBundle getMetrics();
     method public android.media.AudioDeviceInfo getPreferredDevice();
     method public android.media.AudioDeviceInfo getRoutedDevice();
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices();
     method public android.view.Surface getSurface();
     method public boolean isPrivacySensitive();
     method public void pause() throws java.lang.IllegalStateException;
@@ -28529,6 +28579,8 @@
     method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback);
     method public void setOnUnhandledInputEventListener(@NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
     method public boolean setTvView(@Nullable android.media.tv.TvView);
+    method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void setZOrderMediaOverlay(boolean);
+    method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void setZOrderOnTop(boolean);
     method public void startAdService();
     method public void stopAdService();
     field public static final String ERROR_KEY_ERROR_CODE = "error_code";
@@ -28801,6 +28853,8 @@
     method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener);
     method public void setTeletextAppEnabled(boolean);
     method public int setTvView(@Nullable android.media.tv.TvView);
+    method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setZOrderMediaOverlay(boolean);
+    method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setZOrderOnTop(boolean);
     method public void startInteractiveApp();
     method public void stopInteractiveApp();
     field public static final String BI_INTERACTIVE_APP_KEY_ALIAS = "alias";
@@ -29840,6 +29894,7 @@
     method @NonNull public java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate> getVcnUnderlyingNetworkPriorities();
     method public boolean hasGatewayOption(int);
     method @FlaggedApi("android.net.vcn.safe_mode_config") public boolean isSafeModeEnabled();
+    field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1; // 0xffffffff
     field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0
   }
 
@@ -33313,6 +33368,14 @@
     method public final android.os.CountDownTimer start();
   }
 
+  @FlaggedApi("android.os.cpu_gpu_headrooms") public final class CpuHeadroomParams {
+    ctor public CpuHeadroomParams();
+    method public int getCalculationType();
+    method public void setCalculationType(int);
+    field public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
+    field public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
+  }
+
   public final class CpuUsageInfo implements android.os.Parcelable {
     method public int describeContents();
     method public long getActive();
@@ -33560,6 +33623,14 @@
     method public void onProgress(long);
   }
 
+  @FlaggedApi("android.os.cpu_gpu_headrooms") public final class GpuHeadroomParams {
+    ctor public GpuHeadroomParams();
+    method public int getCalculationType();
+    method public void setCalculationType(int);
+    field public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
+    field public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
+  }
+
   public class Handler {
     ctor @Deprecated public Handler();
     ctor @Deprecated public Handler(@Nullable android.os.Handler.Callback);
@@ -34810,6 +34881,10 @@
   }
 
   public class SystemHealthManager {
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getCpuHeadroom(@Nullable android.os.CpuHeadroomParams);
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getCpuHeadroomMinIntervalMillis();
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getGpuHeadroom(@Nullable android.os.GpuHeadroomParams);
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getGpuHeadroomMinIntervalMillis();
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
     method public android.os.health.HealthStats takeMyUidSnapshot();
@@ -46672,6 +46747,8 @@
     method public long getDataUsageBytes();
     method public long getDataUsageTime();
     method @NonNull public int[] getNetworkTypes();
+    method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") @Nullable public java.time.ZonedDateTime getPlanEndDate();
+    method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public int getSubscriptionStatus();
     method @Nullable public CharSequence getSummary();
     method @Nullable public CharSequence getTitle();
     method public void writeToParcel(android.os.Parcel, int);
@@ -46682,6 +46759,11 @@
     field public static final int LIMIT_BEHAVIOR_DISABLED = 0; // 0x0
     field public static final int LIMIT_BEHAVIOR_THROTTLED = 2; // 0x2
     field public static final int LIMIT_BEHAVIOR_UNKNOWN = -1; // 0xffffffff
+    field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_ACTIVE = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_INACTIVE = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_SUSPENDED = 4; // 0x4
+    field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_TRIAL = 3; // 0x3
+    field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_UNKNOWN = 0; // 0x0
     field public static final long TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
   }
 
@@ -46693,6 +46775,7 @@
     method public android.telephony.SubscriptionPlan.Builder setDataLimit(long, int);
     method public android.telephony.SubscriptionPlan.Builder setDataUsage(long, long);
     method @NonNull public android.telephony.SubscriptionPlan.Builder setNetworkTypes(@NonNull int[]);
+    method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") @NonNull public android.telephony.SubscriptionPlan.Builder setSubscriptionStatus(int);
     method public android.telephony.SubscriptionPlan.Builder setSummary(@Nullable CharSequence);
     method public android.telephony.SubscriptionPlan.Builder setTitle(@Nullable CharSequence);
   }
@@ -53168,6 +53251,7 @@
     method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setFrameTimeline(long);
     method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
     method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int);
+    method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public android.view.SurfaceControl.Transaction setLuts(@NonNull android.view.SurfaceControl, @Nullable android.hardware.DisplayLuts);
     method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean);
     method @NonNull public android.view.SurfaceControl.Transaction setPosition(@NonNull android.view.SurfaceControl, float, float);
     method @NonNull public android.view.SurfaceControl.Transaction setScale(@NonNull android.view.SurfaceControl, float, float);
@@ -56960,6 +57044,7 @@
     method @NonNull public java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>> getSupportedHandwritingGesturePreviews();
     method @NonNull public java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>> getSupportedHandwritingGestures();
     method @FlaggedApi("android.view.inputmethod.editorinfo_handwriting_enabled") public boolean isStylusHandwritingEnabled();
+    method @FlaggedApi("android.view.inputmethod.writing_tools") public boolean isWritingToolsEnabled();
     method public final void makeCompatible(int);
     method @FlaggedApi("android.view.inputmethod.public_autofill_id_in_editorinfo") public void setAutofillId(@Nullable android.view.autofill.AutofillId);
     method public void setInitialSurroundingSubText(@NonNull CharSequence, int);
@@ -56968,6 +57053,7 @@
     method @FlaggedApi("android.view.inputmethod.editorinfo_handwriting_enabled") public void setStylusHandwritingEnabled(boolean);
     method public void setSupportedHandwritingGesturePreviews(@NonNull java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>>);
     method public void setSupportedHandwritingGestures(@NonNull java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>>);
+    method @FlaggedApi("android.view.inputmethod.writing_tools") public void setWritingToolsEnabled(boolean);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.EditorInfo> CREATOR;
     field public static final int IME_ACTION_DONE = 6; // 0x6
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index bc73220..1a949d84 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -251,6 +251,10 @@
 
 package android.net {
 
+  @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class ConnectivityFrameworkInitializerBaklava {
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static void registerServiceWrappers();
+  }
+
   public class LocalSocket implements java.io.Closeable {
     ctor public LocalSocket(@NonNull java.io.FileDescriptor);
   }
@@ -310,6 +314,25 @@
 
 }
 
+package android.net.vcn {
+
+  @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class VcnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int describeContents();
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public long getApplicableRedactions();
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int getMinUdpPort4500NatTimeoutSeconds();
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.TransportInfo makeCopy(long);
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public static final android.os.Parcelable.Creator<android.net.vcn.VcnTransportInfo> CREATOR;
+  }
+
+  @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final class VcnTransportInfo.Builder {
+    ctor @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public VcnTransportInfo.Builder();
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo build();
+    method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo.Builder setMinUdpPort4500NatTimeoutSeconds(@IntRange(from=0x78) int);
+  }
+
+}
+
 package android.net.wifi {
 
   public final class WifiMigration {
@@ -379,6 +402,13 @@
     field public static final int DEVICE_INITIAL_SDK_INT;
   }
 
+  public final class Bundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable {
+    method @FlaggedApi("android.os.enable_has_binders") public int hasBinders();
+    field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_NOT_PRESENT = 0; // 0x0
+    field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_PRESENT = 1; // 0x1
+    field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_UNKNOWN = 2; // 0x2
+  }
+
   public class Handler {
     method @FlaggedApi("android.os.mainline_vcn_platform_api") public final boolean hasMessagesOrCallbacks();
     method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeCallbacksAndEqualMessages(@Nullable Object);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ed95fdd..a46f872 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -65,6 +65,7 @@
     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";
@@ -209,6 +210,7 @@
     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";
@@ -304,6 +306,7 @@
     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";
@@ -5045,15 +5048,27 @@
 
 package android.hardware.camera2 {
 
+  public final class CameraCharacteristics extends android.hardware.camera2.CameraMetadata<android.hardware.camera2.CameraCharacteristics.Key<?>> {
+    field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.SharedSessionConfiguration> SHARED_SESSION_CONFIGURATION;
+  }
+
   public abstract class CameraDevice implements java.lang.AutoCloseable {
     method @Deprecated public abstract void createCustomCaptureSession(android.hardware.camera2.params.InputConfiguration, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, int, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback, @Nullable android.os.Handler) throws android.hardware.camera2.CameraAccessException;
     field public static final int SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED = 1; // 0x1
     field public static final int SESSION_OPERATION_MODE_NORMAL = 0; // 0x0
+    field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public static final int SESSION_OPERATION_MODE_SHARED = 2; // 0x2
     field public static final int SESSION_OPERATION_MODE_VENDOR_START = 32768; // 0x8000
   }
 
+  public abstract static class CameraDevice.StateCallback {
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public void onClientSharedAccessPriorityChanged(@NonNull android.hardware.camera2.CameraDevice, boolean);
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public void onOpenedInSharedMode(@NonNull android.hardware.camera2.CameraDevice, boolean);
+  }
+
   public final class CameraManager {
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public boolean isCameraDeviceSharingSupported(@NonNull String) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openSharedCamera(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
   }
 
   public abstract static class CameraManager.AvailabilityCallback {
@@ -5061,6 +5076,12 @@
     method @RequiresPermission(android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER) public void onCameraOpened(@NonNull String, @NonNull String);
   }
 
+  @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract class CameraSharedCaptureSession extends android.hardware.camera2.CameraCaptureSession {
+    ctor public CameraSharedCaptureSession();
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract int startStreaming(@NonNull java.util.List<android.view.Surface>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.CaptureCallback) throws android.hardware.camera2.CameraAccessException;
+    method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract void stopStreaming() throws android.hardware.camera2.CameraAccessException;
+  }
+
 }
 
 package android.hardware.camera2.extension {
@@ -5169,6 +5190,50 @@
     field public static final int ROTATION_90 = 1; // 0x1
   }
 
+  public final class SessionConfiguration implements android.os.Parcelable {
+    field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public static final int SESSION_SHARED = 2; // 0x2
+  }
+
+  @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public final class SharedSessionConfiguration {
+    method @Nullable public android.graphics.ColorSpace getColorSpace();
+    method @NonNull public java.util.List<android.hardware.camera2.params.SharedSessionConfiguration.SharedOutputConfiguration> getOutputStreamsInformation();
+  }
+
+  public static final class SharedSessionConfiguration.SharedOutputConfiguration {
+    method public int getDataspace();
+    method public int getFormat();
+    method public int getMirrorMode();
+    method @Nullable public String getPhysicalCameraId();
+    method @NonNull public android.util.Size getSize();
+    method public long getStreamUseCase();
+    method public int getSurfaceType();
+    method public int getTimestampBase();
+    method public long getUsage();
+    method public boolean isReadoutTimestampEnabled();
+  }
+
+}
+
+package android.hardware.contexthub {
+
+  @FlaggedApi("android.chre.flags.offload_api") public class HubDiscoveryInfo {
+    method @NonNull public android.hardware.contexthub.HubEndpointInfo getHubEndpointInfo();
+  }
+
+  @FlaggedApi("android.chre.flags.offload_api") public final class HubEndpointInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier getIdentifier();
+    method @NonNull public String getName();
+    method @Nullable public String getTag();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.contexthub.HubEndpointInfo> CREATOR;
+  }
+
+  public static class HubEndpointInfo.HubEndpointIdentifier {
+    method public long getEndpoint();
+    method public long getHub();
+  }
+
 }
 
 package android.hardware.devicestate {
@@ -6162,6 +6227,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubClient createClient(@NonNull android.hardware.location.ContextHubInfo, @NonNull android.app.PendingIntent, long);
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> disableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> enableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
+    method @FlaggedApi("android.chre.flags.offload_api") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public java.util.List<android.hardware.contexthub.HubDiscoveryInfo> findEndpoints(long);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] findNanoAppOnHub(int, @NonNull android.hardware.location.NanoAppFilter);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] getContextHubHandles();
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubInfo getContextHubInfo(int);
@@ -7464,6 +7530,7 @@
   }
 
   public final class AudioPlaybackConfiguration implements android.os.Parcelable {
+    method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceInfo> getAudioDeviceInfos();
     method public int getChannelMask();
     method public int getClientPid();
     method public int getClientUid();
@@ -7618,7 +7685,7 @@
 
   public final class MediaCas implements java.lang.AutoCloseable {
     method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean);
-    method @FlaggedApi("com.android.media.flags.update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int);
+    method @FlaggedApi("android.media.tv.flags.mediacas_update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int);
   }
 
   public final class MediaCodec {
@@ -12570,6 +12637,29 @@
 
 }
 
+package android.security.forensic {
+
+  @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>);
+    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
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int STATE_DISABLED = 1; // 0x1
+    field public static final int STATE_ENABLED = 2; // 0x2
+    field public static final int STATE_UNKNOWN = 0; // 0x0
+  }
+
+  public static interface ForensicManager.CommandCallback {
+    method public void onFailure(int);
+    method public void onSuccess();
+  }
+
+}
+
 package android.security.keystore {
 
   public class AndroidKeyStoreProvider extends java.security.Provider {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index cf5ebbaa3..bc38294 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -232,6 +232,8 @@
         "android.hardware.power-aidl",
     ],
     srcs: [
+        "android/os/CpuHeadroomParamsInternal.aidl",
+        "android/os/GpuHeadroomParamsInternal.aidl",
         "android/os/IHintManager.aidl",
         "android/os/IHintSession.aidl",
     ],
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 8b37dbd..6c03b32 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1624,9 +1624,13 @@
     /** @hide Access to read oxygen saturation. */
     public static final int OP_READ_OXYGEN_SATURATION = AppOpEnums.APP_OP_READ_OXYGEN_SATURATION;
 
+    /** @hide Access to write system preferences. */
+    public static final int OP_WRITE_SYSTEM_PREFERENCES =
+            AppOpEnums.APP_OP_WRITE_SYSTEM_PREFERENCES;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 153;
+    public static final int _NUM_OP = 154;
 
     /**
      * All app ops represented as strings.
@@ -1783,6 +1787,7 @@
             OPSTR_READ_SKIN_TEMPERATURE,
             OPSTR_RANGING,
             OPSTR_READ_OXYGEN_SATURATION,
+            OPSTR_WRITE_SYSTEM_PREFERENCES,
     })
     public @interface AppOpString {}
 
@@ -2540,6 +2545,9 @@
     @FlaggedApi(Flags.FLAG_RANGING_PERMISSION_ENABLED)
     public static final String OPSTR_RANGING = "android:ranging";
 
+    /** @hide Access to system preferences write services */
+    public static final String OPSTR_WRITE_SYSTEM_PREFERENCES = "android:write_system_preferences";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2656,6 +2664,7 @@
             OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
             OP_MEDIA_ROUTING_CONTROL,
             OP_READ_SYSTEM_GRAMMATICAL_GENDER,
+            OP_WRITE_SYSTEM_PREFERENCES,
     };
 
     @SuppressWarnings("FlaggedApi")
@@ -3144,6 +3153,10 @@
                 Flags.replaceBodySensorPermissionEnabled()
                     ? HealthPermissions.READ_OXYGEN_SATURATION : null)
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_SYSTEM_PREFERENCES, OPSTR_WRITE_SYSTEM_PREFERENCES,
+            "WRITE_SYSTEM_PREFERENCES").setPermission(
+                     com.android.settingslib.flags.Flags.writeSystemPreferencePermissionEnabled()
+                     ? Manifest.permission.WRITE_SYSTEM_PREFERENCES : null).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 9615015..7a329cd 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -112,8 +112,8 @@
     oneway void requestMultiwindowFullscreen(in IBinder token, in int request,
             in IRemoteCallback callback);
 
-    oneway void startLockTaskModeByToken(in IBinder token);
-    oneway void stopLockTaskModeByToken(in IBinder token);
+    void startLockTaskModeByToken(in IBinder token);
+    void stopLockTaskModeByToken(in IBinder token);
     oneway void showLockTaskEscapeMessage(in IBinder token);
     void setTaskDescription(in IBinder token, in ActivityManager.TaskDescription values);
 
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index e451116..a063917 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -163,6 +163,7 @@
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
 import android.nearby.NearbyFrameworkInitializer;
 import android.net.ConnectivityFrameworkInitializer;
+import android.net.ConnectivityFrameworkInitializerBaklava;
 import android.net.ConnectivityFrameworkInitializerTiramisu;
 import android.net.INetworkPolicyManager;
 import android.net.IPacProxyManager;
@@ -173,7 +174,6 @@
 import android.net.PacProxyManager;
 import android.net.TetheringManager;
 import android.net.VpnManager;
-import android.net.vcn.VcnFrameworkInitializer;
 import android.net.wifi.WifiFrameworkInitializer;
 import android.net.wifi.nl80211.WifiNl80211Manager;
 import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
@@ -190,6 +190,7 @@
 import android.os.IBinder;
 import android.os.IDumpstate;
 import android.os.IHardwarePropertiesManager;
+import android.os.IHintManager;
 import android.os.IPowerManager;
 import android.os.IPowerStatsService;
 import android.os.IRecoverySystem;
@@ -238,6 +239,8 @@
 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.keystore.KeyStoreManager;
 import android.service.oemlock.IOemLockService;
 import android.service.oemlock.OemLockManager;
@@ -1195,8 +1198,10 @@
             public SystemHealthManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                 IBinder batteryStats = ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME);
                 IBinder powerStats = ServiceManager.getService(Context.POWER_STATS_SERVICE);
+                IBinder perfHint = ServiceManager.getService(Context.PERFORMANCE_HINT_SERVICE);
                 return new SystemHealthManager(IBatteryStats.Stub.asInterface(batteryStats),
-                        IPowerStatsService.Stub.asInterface(powerStats));
+                        IPowerStatsService.Stub.asInterface(powerStats),
+                        IHintManager.Stub.asInterface(perfHint));
             }});
 
         registerService(Context.CONTEXTHUB_SERVICE, ContextHubManager.class,
@@ -1790,6 +1795,18 @@
                     }
                 });
 
+        registerService(Context.FORENSIC_SERVICE, ForensicManager.class,
+                new CachedServiceFetcher<ForensicManager>() {
+                    @Override
+                    public ForensicManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder b = ServiceManager.getServiceOrThrow(
+                                Context.FORENSIC_SERVICE);
+                        IForensicService service = IForensicService.Stub.asInterface(b);
+                        return new ForensicManager(service);
+                    }
+                });
+
         sInitializing = true;
         try {
             // Note: the following functions need to be @SystemApis, once they become mainline
@@ -1818,7 +1835,7 @@
             OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers();
             DeviceLockFrameworkInitializer.registerServiceWrappers();
             VirtualizationFrameworkInitializer.registerServiceWrappers();
-            VcnFrameworkInitializer.registerServiceWrappers();
+            ConnectivityFrameworkInitializerBaklava.registerServiceWrappers();
 
             if (com.android.server.telecom.flags.Flags.telecomMainlineBlockedNumbersManager()) {
                 ProviderFrameworkInitializer.registerServiceWrappers();
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 404471e..0088925 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -343,6 +343,16 @@
 }
 
 flag {
+    name: "active_admin_cleanup"
+    namespace: "enterprise"
+    description: "Remove ActiveAdmin from EnforcingAdmin and related cleanups"
+    bug: "335663055"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "user_provisioning_same_state"
     namespace: "enterprise"
     description: "Handle exceptions while setting same provisioning state."
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/core/java/android/app/supervision/SupervisionManagerInternal.java
similarity index 84%
rename from services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
rename to core/java/android/app/supervision/SupervisionManagerInternal.java
index 5df9dd5..d571e14 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
+++ b/core/java/android/app/supervision/SupervisionManagerInternal.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.server.supervision;
+package android.app.supervision;
 
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 
 /**
  * Local system service interface for {@link SupervisionService}.
@@ -35,6 +35,11 @@
     public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
 
     /**
+     * Returns whether the supervision lock screen needs to be shown.
+     */
+    public abstract boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId);
+
+    /**
      * Set whether supervision is enabled for the specified user.
      *
      * @param userId The user to set the supervision state for
@@ -50,5 +55,5 @@
      * @param options Optional configuration parameters for the supervision lock screen
      */
     public abstract void setSupervisionLockscreenEnabledForUser(
-            @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+            @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options);
 }
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 3152ff4..13b13b9 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -292,6 +292,10 @@
      * <p>
      * The value of a property will only have a single type, as defined by
      * the property itself.
+     *
+     * <p class="note"><strong>Note:</strong>
+     * In android version {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and earlier,
+     * the {@code equals} and {@code hashCode} methods for this class may not function as expected.
      */
     public static final class Property implements Parcelable {
         private static final int TYPE_BOOLEAN = 1;
@@ -523,6 +527,40 @@
                 return new Property[size];
             }
         };
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof Property)) {
+                return false;
+            }
+            final Property property = (Property) obj;
+            return mType == property.mType &&
+                    Objects.equals(mName, property.mName) &&
+                    Objects.equals(mClassName, property.mClassName) &&
+                    Objects.equals(mPackageName, property.mPackageName) &&
+                    (mType == TYPE_BOOLEAN ? mBooleanValue == property.mBooleanValue :
+                     mType == TYPE_FLOAT ? Float.compare(mFloatValue, property.mFloatValue) == 0 :
+                     mType == TYPE_INTEGER ? mIntegerValue == property.mIntegerValue :
+                     mType == TYPE_RESOURCE ? mIntegerValue == property.mIntegerValue :
+                     mStringValue.equals(property.mStringValue));
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(mName, mType, mClassName, mPackageName);
+            if (mType == TYPE_BOOLEAN) {
+                result = 31 * result + (mBooleanValue ? 1 : 0);
+            } else if (mType == TYPE_FLOAT) {
+                result = 31 * result + Float.floatToIntBits(mFloatValue);
+            } else if (mType == TYPE_INTEGER) {
+                result = 31 * result + mIntegerValue;
+            } else if (mType == TYPE_RESOURCE) {
+                result = 31 * result + mIntegerValue;
+            } else if (mType == TYPE_STRING) {
+                result = 31 * result + mStringValue.hashCode();
+            }
+            return result;
+        }
     }
 
     /**
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 9ba5a35..e181ae8 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -366,3 +366,13 @@
     description: "Block app installations that specify an incompatible minor SDK version"
     bug: "377474232"
 }
+
+flag {
+    name: "app_compat_option_16kb"
+    is_exported: true
+    namespace: "devoptions_settings"
+    description: "Feature flag to enable page size app compat mode from manifest, package manager and settings level."
+    bug: "371049373"
+    is_fixed_read_only: true
+}
+
diff --git a/core/java/android/hardware/DisplayLuts.java b/core/java/android/hardware/DisplayLuts.java
index b162ad6..6343ba1 100644
--- a/core/java/android/hardware/DisplayLuts.java
+++ b/core/java/android/hardware/DisplayLuts.java
@@ -16,116 +16,294 @@
 
 package android.hardware;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.hardware.flags.Flags;
 import android.util.IntArray;
 
 import java.util.ArrayList;
-import java.util.List;
 
 /**
- * @hide
+ * DisplayLuts provides the developers to apply Lookup Tables (Luts) to a
+ * {@link android.view.SurfaceControl}. Luts provides ways to control tonemapping
+ * for specific content.
+ *
+ * The general flow is as follows:
+ * <p>
+ *      <img src="{@docRoot}reference/android/images/graphics/DisplayLuts.png" />
+ *      <figcaption style="text-align: center;">DisplayLuts flow</figcaption>
+ * </p>
+ *
+ * @see LutProperties
  */
+@FlaggedApi(Flags.FLAG_LUTS_API)
 public final class DisplayLuts {
+    private ArrayList<Entry> mEntries;
     private IntArray mOffsets;
     private int mTotalLength;
 
-    private List<float[]> mLutBuffers;
-    private IntArray mLutDimensions;
-    private IntArray mLutSizes;
-    private IntArray mLutSamplingKeys;
-    private static final int LUT_LENGTH_LIMIT = 100000;
-
+    /**
+     * Create a {@link DisplayLuts} instance.
+     */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public DisplayLuts() {
+        mEntries = new ArrayList<>();
         mOffsets = new IntArray();
         mTotalLength = 0;
-
-        mLutBuffers = new ArrayList<>();
-        mLutDimensions = new IntArray();
-        mLutSizes = new IntArray();
-        mLutSamplingKeys = new IntArray();
     }
 
-    /**
-     * Add the lut to be applied.
-     *
-     * @param buffer
-     * @param dimension either 1D or 3D
-     * @param size
-     * @param samplingKey
-     */
-    public void addLut(@NonNull float[] buffer, @LutProperties.Dimension int dimension,
-                       int size, @LutProperties.SamplingKey int samplingKey) {
+    @FlaggedApi(Flags.FLAG_LUTS_API)
+    public static class Entry {
+        private float[] mBuffer;
+        private @LutProperties.Dimension int mDimension;
+        private int mSize;
+        private @LutProperties.SamplingKey int mSamplingKey;
 
-        int lutLength = 0;
-        if (dimension == LutProperties.ONE_DIMENSION) {
-            lutLength = size;
-        } else if (dimension == LutProperties.THREE_DIMENSION) {
-            lutLength = size * size * size;
-        } else {
-            clear();
-            throw new IllegalArgumentException("The dimension is either 1D or 3D!");
+        private static final int LUT_LENGTH_LIMIT = 100000;
+
+        /**
+         * Create a Lut entry.
+         *
+         * <p>
+         * Noted that 1D Lut(s) are treated as gain curves.
+         * For 3D Lut(s), 3D Lut(s) are used for direct color manipulations.
+         * The values of 3D Lut(s) data should be normalized to the range {@code 0.0}
+         * to {@code 1.0}, inclusive. And 3D Lut(s) data is organized in the order of
+         * R, G, B channels.
+         *
+         * @param buffer The raw lut data
+         * @param dimension Either 1D or 3D
+         * @param samplingKey The sampling kay used for the Lut
+         */
+        @FlaggedApi(Flags.FLAG_LUTS_API)
+        public Entry(@NonNull float[] buffer,
+                    @LutProperties.Dimension int dimension,
+                    @LutProperties.SamplingKey int samplingKey) {
+            if (buffer == null || buffer.length < 1) {
+                throw new IllegalArgumentException("The buffer cannot be empty!");
+            }
+
+            if (buffer.length >= LUT_LENGTH_LIMIT) {
+                throw new IllegalArgumentException("The lut length is too big to handle!");
+            }
+
+            if (dimension != LutProperties.ONE_DIMENSION
+                    && dimension != LutProperties.THREE_DIMENSION) {
+                throw new IllegalArgumentException("The dimension should be either 1D or 3D!");
+            }
+
+            if (dimension == LutProperties.THREE_DIMENSION) {
+                if (buffer.length <= 3) {
+                    throw new IllegalArgumentException(
+                            "The 3d lut size of each dimension should be over 1!");
+                }
+                int lengthPerChannel = buffer.length;
+                if (lengthPerChannel % 3 != 0) {
+                    throw new IllegalArgumentException(
+                            "The lut buffer of 3dlut should have 3 channels!");
+                }
+                lengthPerChannel /= 3;
+
+                double size = Math.cbrt(lengthPerChannel);
+                if (size == (int) size) {
+                    mSize = (int) size;
+                } else {
+                    throw new IllegalArgumentException(
+                            "Cannot get the cube root of the 3d lut buffer!");
+                }
+            } else {
+                mSize = buffer.length;
+            }
+
+            mBuffer = buffer;
+            mDimension = dimension;
+            mSamplingKey = samplingKey;
         }
 
-        if (lutLength >= LUT_LENGTH_LIMIT) {
-            clear();
-            throw new IllegalArgumentException("The lut length is too big to handle!");
+        /**
+         * @return the dimension of the lut entry
+         */
+        @FlaggedApi(Flags.FLAG_LUTS_API)
+        public int getDimension() {
+            return mDimension;
         }
 
+        /**
+         * @return the size of the lut for each dimension
+         * @hide
+         */
+        public int getSize() {
+            return mSize;
+        }
+
+        /**
+         * @return the lut raw data of the lut
+         */
+        @FlaggedApi(Flags.FLAG_LUTS_API)
+        public @NonNull float[] getBuffer() {
+            return mBuffer;
+        }
+
+        /**
+         * @return the sampling key used by the lut
+         */
+        @FlaggedApi(Flags.FLAG_LUTS_API)
+        public int getSamplingKey() {
+            return mSamplingKey;
+        }
+
+        @Override
+        public String toString() {
+            return "Entry{"
+                    + "dimension=" + DisplayLuts.Entry.dimensionToString(getDimension())
+                    + ", size(each dimension)=" + getSize()
+                    + ", samplingKey=" + samplingKeyToString(getSamplingKey()) + "}";
+        }
+
+        private static String dimensionToString(int dimension) {
+            switch(dimension) {
+                case LutProperties.ONE_DIMENSION:
+                    return "ONE_DIMENSION";
+                case LutProperties.THREE_DIMENSION:
+                    return "THREE_DIMENSION";
+                default:
+                    return "";
+            }
+        }
+
+        private static String samplingKeyToString(int key) {
+            switch(key) {
+                case LutProperties.SAMPLING_KEY_RGB:
+                    return "SAMPLING_KEY_RGB";
+                case LutProperties.SAMPLING_KEY_MAX_RGB:
+                    return "SAMPLING_KEY_MAX_RGB";
+                default:
+                    return "";
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("DisplayLuts{");
+        sb.append("\n");
+        for (DisplayLuts.Entry entry: mEntries) {
+            sb.append(entry.toString());
+            sb.append("\n");
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private void addEntry(Entry entry) {
+        mEntries.add(entry);
         mOffsets.add(mTotalLength);
-        mTotalLength += lutLength;
-
-        mLutBuffers.add(buffer);
-        mLutDimensions.add(dimension);
-        mLutSizes.add(size);
-        mLutSamplingKeys.add(samplingKey);
+        mTotalLength += entry.getBuffer().length;
     }
 
     private void clear() {
-        mTotalLength = 0;
         mOffsets.clear();
-        mLutBuffers.clear();
-        mLutDimensions.clear();
-        mLutSamplingKeys.clear();
+        mTotalLength = 0;
+        mEntries.clear();
     }
 
     /**
-     * @return the array of Lut buffers
+     * Set a Lut to be applied.
+     *
+     * <p>Use either this or {@link #set(Entry, Entry)}. The function will
+     * replace any previously set lut(s).</p>
+     *
+     * @param entry Either an 1D Lut or a 3D Lut
+     */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
+    public void set(@NonNull Entry entry) {
+        if (entry == null) {
+            throw new IllegalArgumentException("The entry is null!");
+        }
+        clear();
+        addEntry(entry);
+    }
+
+    /**
+     * Set Luts in order to be applied.
+     *
+     * <p> An 1D Lut and 3D Lut will be applied in order. Use either this or
+     * {@link #set(Entry)}. The function will replace any previously set lut(s)</p>
+     *
+     * @param first An 1D Lut
+     * @param second A 3D Lut
+     */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
+    public void set(@NonNull Entry first, @NonNull Entry second) {
+        if (first == null || second == null) {
+            throw new IllegalArgumentException("The entry is null!");
+        }
+        if (first.getDimension() != LutProperties.ONE_DIMENSION
+                || second.getDimension() != LutProperties.THREE_DIMENSION) {
+            throw new IllegalArgumentException("The entries should be 1D and 3D in order!");
+        }
+        clear();
+        addEntry(first);
+        addEntry(second);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean valid() {
+        return mEntries.size() > 0;
+    }
+
+    /**
+     * @hide
      */
     public float[] getLutBuffers() {
         float[] buffer = new float[mTotalLength];
 
-        for (int i = 0; i < mLutBuffers.size(); i++) {
-            float[] lutBuffer = mLutBuffers.get(i);
+        for (int i = 0; i < mEntries.size(); i++) {
+            float[] lutBuffer = mEntries.get(i).getBuffer();
             System.arraycopy(lutBuffer, 0, buffer, mOffsets.get(i), lutBuffer.length);
         }
         return buffer;
     }
 
     /**
-     * @return the starting point of each lut memory region of the lut buffer
+     * @hide
      */
     public int[] getOffsets() {
         return mOffsets.toArray();
     }
 
     /**
-     * @return the array of Lut size
+     * @hide
      */
     public int[] getLutSizes() {
-        return mLutSizes.toArray();
+        int[] sizes = new int[mEntries.size()];
+        for (int i = 0; i < mEntries.size(); i++) {
+            sizes[i] = mEntries.get(i).getSize();
+        }
+        return sizes;
     }
 
     /**
-     * @return the array of Lut dimension
+     * @hide
      */
     public int[] getLutDimensions() {
-        return mLutDimensions.toArray();
+        int[] dimensions = new int[mEntries.size()];
+        for (int i = 0; i < mEntries.size(); i++) {
+            dimensions[i] = mEntries.get(i).getDimension();
+        }
+        return dimensions;
     }
 
     /**
-     * @return the array of sampling key
+     * @hide
      */
     public int[] getLutSamplingKeys() {
-        return mLutSamplingKeys.toArray();
+        int[] samplingKeys = new int[mEntries.size()];
+        for (int i = 0; i < mEntries.size(); i++) {
+            samplingKeys[i] = mEntries.get(i).getSamplingKey();
+        }
+        return samplingKeys;
     }
 }
diff --git a/core/java/android/hardware/LutProperties.java b/core/java/android/hardware/LutProperties.java
index c9c6d6d..bf40a41 100644
--- a/core/java/android/hardware/LutProperties.java
+++ b/core/java/android/hardware/LutProperties.java
@@ -16,23 +16,31 @@
 
 package android.hardware;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.hardware.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Lut properties class.
+ * Provides Lut properties of the device.
  *
- * A Lut (Look-Up Table) is a pre-calculated table for color transformation.
- *
- * @hide
+ * <p>
+ * A Lut (Look-Up Table) is a pre-calculated table for color correction.
+ * Applications may be interested in the Lut properties exposed by
+ * this class to determine if the Lut(s) they select using
+ * {@link android.view.SurfaceControl.Transaction#setLuts} are by the HWC.
+ * </p>
  */
+@FlaggedApi(Flags.FLAG_LUTS_API)
 public final class LutProperties {
     private final @Dimension int mDimension;
     private final int mSize;
     private final @SamplingKey int[] mSamplingKeys;
 
+    /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"SAMPLING_KEY_"}, value = {
         SAMPLING_KEY_RGB,
@@ -42,11 +50,14 @@
     }
 
     /** use r,g,b channel as the gain value of a Lut */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public static final int SAMPLING_KEY_RGB = 0;
 
     /** use max of r,g,b channel as the gain value of a Lut */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public static final int SAMPLING_KEY_MAX_RGB = 1;
 
+    /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
         ONE_DIMENSION,
@@ -56,18 +67,22 @@
     }
 
     /** The Lut is one dimensional */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public static final int ONE_DIMENSION = 1;
 
     /** The Lut is three dimensional */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public static final int THREE_DIMENSION = 3;
 
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public @Dimension int getDimension() {
         return mDimension;
     }
 
     /**
-     * @return the size of the Lut.
+     * @return the size of the Lut for each dimension
      */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
     public int getSize() {
         return mSize;
     }
@@ -75,6 +90,8 @@
     /**
      * @return the list of sampling keys
      */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
+    @NonNull
     public @SamplingKey int[] getSamplingKeys() {
         if (mSamplingKeys.length == 0) {
             throw new IllegalStateException("no sampling key!");
diff --git a/core/java/android/hardware/OverlayProperties.java b/core/java/android/hardware/OverlayProperties.java
index 24cfc1b..d42bfae 100644
--- a/core/java/android/hardware/OverlayProperties.java
+++ b/core/java/android/hardware/OverlayProperties.java
@@ -18,6 +18,7 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.hardware.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -72,9 +73,11 @@
     }
 
     /**
-     * Gets the lut properties of the display.
-     * @hide
+     * Returns the lut properties of the device.
      */
+    @FlaggedApi(Flags.FLAG_LUTS_API)
+    @SuppressLint("ArrayReturn")
+    @NonNull
     public LutProperties[] getLutProperties() {
         if (mNativeObject == 0) {
             return null;
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 58e524e..5533a64 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -19,9 +19,9 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.impl.ExtensionKey;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
 import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
@@ -6172,6 +6172,66 @@
     public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS_MAXIMUM_RESOLUTION =
             new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRStallDurationsMaximumResolution", android.hardware.camera2.params.StreamConfigurationDuration[].class);
 
+    /**
+     * <p>Color space used for shared session configuration for all the output targets
+     * when camera is opened in shared mode. This should be one of the values specified in
+     * availableColorSpaceProfilesMap.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #SHARED_SESSION_COLOR_SPACE_UNSPECIFIED UNSPECIFIED}</li>
+     *   <li>{@link #SHARED_SESSION_COLOR_SPACE_SRGB SRGB}</li>
+     *   <li>{@link #SHARED_SESSION_COLOR_SPACE_DISPLAY_P3 DISPLAY_P3}</li>
+     *   <li>{@link #SHARED_SESSION_COLOR_SPACE_BT2020_HLG BT2020_HLG}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @see #SHARED_SESSION_COLOR_SPACE_UNSPECIFIED
+     * @see #SHARED_SESSION_COLOR_SPACE_SRGB
+     * @see #SHARED_SESSION_COLOR_SPACE_DISPLAY_P3
+     * @see #SHARED_SESSION_COLOR_SPACE_BT2020_HLG
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final Key<Integer> SHARED_SESSION_COLOR_SPACE =
+            new Key<Integer>("android.sharedSession.colorSpace", int.class);
+
+    /**
+     * <p>List of shared output configurations that this camera device supports when
+     * camera is opened in shared mode. Array contains following entries for each supported
+     * shared configuration:
+     * 1) surface type
+     * 2) width
+     * 3) height
+     * 4) format
+     * 5) mirrorMode
+     * 6) useReadoutTimestamp
+     * 7) timestampBase
+     * 8) dataspace
+     * 9) usage
+     * 10) streamUsecase
+     * 11) physical camera id len
+     * 12) physical camera id as UTF-8 null terminated string.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final Key<long[]> SHARED_SESSION_OUTPUT_CONFIGURATIONS =
+            new Key<long[]>("android.sharedSession.outputConfigurations", long[].class);
+
+    /**
+     * <p>The available stream configurations that this camera device supports for
+     * shared capture session when camera is opened in shared mode. Android camera framework
+     * will generate this tag if the camera device can be opened in shared mode.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @SyntheticKey
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final Key<android.hardware.camera2.params.SharedSessionConfiguration> SHARED_SESSION_CONFIGURATION =
+            new Key<android.hardware.camera2.params.SharedSessionConfiguration>("android.sharedSession.configuration", android.hardware.camera2.params.SharedSessionConfiguration.class);
+
 
     /**
      * Mapping from INFO_SESSION_CONFIGURATION_QUERY_VERSION to session characteristics key.
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index fb381d9..852f047 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -446,6 +446,17 @@
     public static final int SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED =
             1; // ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE;
 
+     /**
+     * Shared camera operation mode.
+     *
+     * @see #CameraSharedCaptureSession
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final int SESSION_OPERATION_MODE_SHARED =
+            2; // ICameraDeviceUser.SHARED_MODE;
+
     /**
      * First vendor-specific operating mode
      *
@@ -461,6 +472,7 @@
     @IntDef(prefix = {"SESSION_OPERATION_MODE"}, value =
             {SESSION_OPERATION_MODE_NORMAL,
              SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED,
+             SESSION_OPERATION_MODE_SHARED,
              SESSION_OPERATION_MODE_VENDOR_START})
     public @interface SessionOperatingMode {};
 
@@ -1240,7 +1252,6 @@
      *
      * </ul>
      *
-     *
      * @param config A session configuration (see {@link SessionConfiguration}).
      *
      * @throws IllegalArgumentException In case the session configuration is invalid; or the output
@@ -1559,6 +1570,48 @@
         public abstract void onOpened(@NonNull CameraDevice camera); // Must implement
 
         /**
+         * The method called when a camera device has finished opening in shared mode,
+         * where there can be more than one client accessing the same camera.
+         *
+         * <p>At this point, the camera device is ready to use, and
+         * {@link CameraDevice#createCaptureSession} can be called to set up the shared capture
+         * session.</p>
+         *
+         * @param camera the camera device that has become opened
+         * @param isPrimaryClient true if the client opening the camera is currently the primary
+         *                             client.
+         * @hide
+         */
+        @SystemApi
+        @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+        public void onOpenedInSharedMode(@NonNull CameraDevice camera, boolean isPrimaryClient) {
+          // Default empty implementation
+        }
+
+        /**
+         * The method called when client access priorities have changed for a camera device opened
+         * in shared mode where there can be more than one client accessing the same camera.
+         *
+         * If the client priority changed from secondary to primary, then it can now
+         * create capture request and change the capture request parameters. If client priority
+         * changed from primary to secondary, that implies that a higher priority client has also
+         * opened the camera in shared mode and the new client is now a primary client
+         *
+         * @param camera the camera device whose access priorities have changed.
+         * @param isPrimaryClient true if the client is now the primary client.
+         *                        false if another higher priority client also opened the
+         *                        camera and is now the new primary client and this client is
+         *                        now a secondary client.
+         * @hide
+         */
+        @SystemApi
+        @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+        public void onClientSharedAccessPriorityChanged(@NonNull CameraDevice camera,
+                boolean isPrimaryClient) {
+          // Default empty implementation
+        }
+
+        /**
          * The method called when a camera device has been closed with
          * {@link CameraDevice#close}.
          *
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 75e2058..266efb7 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -976,6 +976,46 @@
     }
 
     /**
+     * Checks if a camera device can be opened in a shared mode for a given {@code cameraId}.
+     * If this method returns false for a {@code cameraId}, calling {@link #openSharedCamera}
+     * for that {@code cameraId} will throw an {@link UnsupportedOperationException}.
+     *
+     * @param cameraId The unique identifier of the camera device for which sharing support is
+     *                 being queried. This identifier must be present in
+     *                 {@link #getCameraIdList()}.
+     *
+     * @return {@code true} if camera can be opened in shared mode
+     *                      for the provided {@code cameraId}; {@code false} otherwise.
+     *
+     * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not
+     *                                  match any device in {@link #getCameraIdList()}.
+     * @throws CameraAccessException    if the camera device has been disconnected.
+     *
+     * @see #getCameraIdList()
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    @SystemApi
+    public boolean isCameraDeviceSharingSupported(@NonNull String cameraId)
+            throws CameraAccessException {
+        if (cameraId == null) {
+            throw new IllegalArgumentException("Camera ID was null");
+        }
+
+        if (CameraManagerGlobal.sCameraServiceDisabled
+                || !Arrays.asList(CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext))).contains(cameraId)) {
+            throw new IllegalArgumentException(
+                    "Camera ID '" + cameraId + "' not available on device.");
+        }
+
+        CameraCharacteristics chars = getCameraCharacteristics(cameraId);
+        long[] sharedOutputConfiguration =
+                chars.get(CameraCharacteristics.SHARED_SESSION_OUTPUT_CONFIGURATIONS);
+        return (sharedOutputConfiguration != null);
+    }
+
+    /**
      * Retrieves the AttributionSourceState to pass to the CameraService.
      *
      * @param deviceIdOverride An override of the AttributionSource's deviceId, if not equal to
@@ -1036,6 +1076,9 @@
      * @param cameraId The unique identifier of the camera device to open
      * @param callback The callback for the camera. Must not be null.
      * @param executor The executor to invoke the callback with. Must not be null.
+     * @param oomScoreOffset The minimum oom score that cameraservice must see for this client.
+     * @param rotationOverride The type of rotation override.
+     * @param sharedMode Parameter specifying if the camera should be opened in shared mode.
      *
      * @throws CameraAccessException if the camera is disabled by device policy,
      * too many camera devices are already open, or the cameraId does not match
@@ -1051,7 +1094,8 @@
      */
     private CameraDevice openCameraDeviceUserAsync(String cameraId,
             CameraDevice.StateCallback callback, Executor executor,
-            final int oomScoreOffset, int rotationOverride) throws CameraAccessException {
+            final int oomScoreOffset, int rotationOverride, boolean sharedMode)
+            throws CameraAccessException {
         CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
         CameraDevice device = null;
         synchronized (mLock) {
@@ -1070,7 +1114,7 @@
                         characteristics,
                         this,
                         mContext.getApplicationInfo().targetSdkVersion,
-                        mContext, cameraDeviceSetup);
+                        mContext, cameraDeviceSetup, sharedMode);
             ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks();
 
             try {
@@ -1091,7 +1135,7 @@
                                 mContext.getApplicationInfo().targetSdkVersion,
                                 rotationOverride,
                                 clientAttribution,
-                                getDevicePolicyFromContext(mContext));
+                                getDevicePolicyFromContext(mContext), sharedMode);
             } catch (ServiceSpecificException e) {
                 if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) {
                     throw new AssertionError("Should've gone down the shim path");
@@ -1218,7 +1262,8 @@
             @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)
             throws CameraAccessException {
 
-        openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler));
+        openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
+                /*oomScoreOffset*/0, getRotationOverride(mContext), /*sharedMode*/false);
     }
 
     /**
@@ -1258,7 +1303,7 @@
                          /*oomScoreOffset*/0,
                          overrideToPortrait
                                  ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT
-                                 : ICameraService.ROTATION_OVERRIDE_NONE);
+                                 : ICameraService.ROTATION_OVERRIDE_NONE, /*sharedMode*/false);
     }
 
     /**
@@ -1303,10 +1348,57 @@
         if (executor == null) {
             throw new IllegalArgumentException("executor was null");
         }
-        openCameraImpl(cameraId, callback, executor);
+        openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0,
+                getRotationOverride(mContext), /*sharedMode*/false);
     }
 
     /**
+     * Opens a shared connection to a camera with the given ID.
+     *
+     * <p>The behavior of this method matches that of
+     * {@link #openCamera(String, Executor, StateCallback)}, except that it opens the camera in
+     * shared mode where more than one client can access the camera at the same time.</p>
+     *
+     * @param cameraId The unique identifier of the camera device to open.
+     * @param executor The executor which will be used when invoking the callback.
+     * @param callback The callback which is invoked once the camera is opened
+     *
+     * @throws CameraAccessException if the camera is disabled by device policy, or is being used
+     *                               by a higher-priority client in non-shared mode or the device
+     *                               has reached its maximal resource and cannot open this camera
+     *                               device.
+     *
+     * @throws IllegalArgumentException if cameraId, the callback or the executor was null,
+     *                                  or the cameraId does not match any currently or previously
+     *                                  available camera device.
+     *
+     * @throws SecurityException if the application does not have permission to
+     *                           access the camera
+     *
+     * @see #getCameraIdList
+     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.SYSTEM_CAMERA,
+            android.Manifest.permission.CAMERA,
+    })
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public void openSharedCamera(@NonNull String cameraId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull final CameraDevice.StateCallback callback)
+            throws CameraAccessException {
+        if (executor == null) {
+            throw new IllegalArgumentException("executor was null");
+        }
+        openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0,
+                getRotationOverride(mContext), /*sharedMode*/true);
+    }
+
+
+    /**
      * Open a connection to a camera with the given ID. Also specify what oom score must be offset
      * by cameraserver for this client. This api can be useful for system
      * components which want to assume a lower priority (for camera arbitration) than other clients
@@ -1372,29 +1464,35 @@
                     "oomScoreOffset < 0, cannot increase priority of camera client");
         }
         openCameraImpl(cameraId, callback, executor, oomScoreOffset,
-                getRotationOverride(mContext));
+                getRotationOverride(mContext), /*sharedMode*/false);
     }
 
     /**
      * Open a connection to a camera with the given ID, on behalf of another application.
-     * Also specify the minimum oom score and process state the application
-     * should have, as seen by the cameraserver.
      *
-     * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows
-     * the caller to specify the UID to use for permission/etc verification. This can only be
-     * done by services trusted by the camera subsystem to act on behalf of applications and
-     * to forward the real UID.</p>
-     *
+     * @param cameraId
+     *             The unique identifier of the camera device to open
+     * @param callback
+     *             The callback which is invoked once the camera is opened
+     * @param executor
+     *             The executor which will be used when invoking the callback.
      * @param oomScoreOffset
      *             The minimum oom score that cameraservice must see for this client.
      * @param rotationOverride
      *             The type of rotation override (none, override_to_portrait, rotation_only)
      *             that should be followed for this camera id connection
+     * @param sharedMode
+     *             Parameter specifying if the camera should be opened in shared mode.
+     *
+     * @throws CameraAccessException if the camera is disabled by device policy,
+     * has been disconnected, or is being used by a higher-priority camera API client in
+     * non shared mode.
+     *
      * @hide
      */
     public void openCameraImpl(@NonNull String cameraId,
             @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor,
-            int oomScoreOffset, int rotationOverride)
+            int oomScoreOffset, int rotationOverride, boolean sharedMode)
             throws CameraAccessException {
 
         if (cameraId == null) {
@@ -1407,24 +1505,7 @@
         }
 
         openCameraDeviceUserAsync(cameraId, callback, executor, oomScoreOffset,
-                rotationOverride);
-    }
-
-    /**
-     * Open a connection to a camera with the given ID, on behalf of another application.
-     *
-     * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows
-     * the caller to specify the UID to use for permission/etc verification. This can only be
-     * done by services trusted by the camera subsystem to act on behalf of applications and
-     * to forward the real UID.</p>
-     *
-     * @hide
-     */
-    public void openCameraImpl(@NonNull String cameraId,
-            @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor)
-            throws CameraAccessException {
-        openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0,
-                getRotationOverride(mContext));
+                rotationOverride, sharedMode);
     }
 
     /**
@@ -2541,6 +2622,10 @@
                 }
                 @Override
                 public void onCameraClosed(String id, int deviceId) {
+                }
+                @Override
+                public void onCameraOpenedInSharedMode(String id, String clientPackageId,
+                        int deviceId, boolean primaryClient) {
                 }};
 
             String[] cameraIds;
@@ -3325,6 +3410,11 @@
         }
 
         @Override
+        public void onCameraOpenedInSharedMode(String cameraId, String clientPackageId,
+                int deviceId, boolean primaryClient) {
+        }
+
+        @Override
         public void onCameraOpened(String cameraId, String clientPackageId, int deviceId) {
             synchronized (mLock) {
                 onCameraOpenedLocked(new DeviceCameraInfo(cameraId, deviceId), clientPackageId);
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 8d36fbd..d2fcfd6 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -2121,6 +2121,38 @@
     public static final int AUTOMOTIVE_LOCATION_EXTRA_RIGHT = 10;
 
     //
+    // Enumeration values for CameraCharacteristics#SHARED_SESSION_COLOR_SPACE
+    //
+
+    /**
+     * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final int SHARED_SESSION_COLOR_SPACE_UNSPECIFIED = -1;
+
+    /**
+     * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final int SHARED_SESSION_COLOR_SPACE_SRGB = 0;
+
+    /**
+     * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final int SHARED_SESSION_COLOR_SPACE_DISPLAY_P3 = 7;
+
+    /**
+     * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public static final int SHARED_SESSION_COLOR_SPACE_BT2020_HLG = 16;
+
+    //
     // Enumeration values for CaptureRequest#COLOR_CORRECTION_MODE
     //
 
diff --git a/core/java/android/hardware/camera2/CameraSharedCaptureSession.java b/core/java/android/hardware/camera2/CameraSharedCaptureSession.java
new file mode 100644
index 0000000..5426d4d
--- /dev/null
+++ b/core/java/android/hardware/camera2/CameraSharedCaptureSession.java
@@ -0,0 +1,180 @@
+/*
+ * 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.hardware.camera2;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.view.Surface;
+
+import com.android.internal.camera.flags.Flags;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A shared capture session for a {@link CameraDevice}, when a camera device is opened in shared
+ * mode possibly by multiple clients at the same time.
+ *
+ * <p>An active shared capture session is a special type of capture session used exclusively
+ * for shared camera access by multiple applications, provided the camera device supports this
+ * mode. To determine if a camera device supports shared mode, use the
+ * {@link android.hardware.camera2.CameraManager#isCameraDeviceSharingSupported} API.
+ * If supported, multiple clients can open the camera by calling
+ * {@link android.hardware.camera2.CameraManager#openSharedCamera} and create a shared capture
+ * session by calling {@link CameraDevice#createCaptureSession(SessionConfiguration)} and using
+ * session type as {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED}</p>
+ *
+ * <p>When an application has opened a camera device in shared mode, it can only create a shared
+ * capture session using session type as
+ * {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED}. Any other session
+ * type value will trigger {@link IllegalArgumentException}. Once the configuration is complete and
+ * the session is ready to actually capture data, the provided
+ * {@link CameraCaptureSession.StateCallback}'s
+ * {@link CameraCaptureSession.StateCallback#onConfigured} callback will be called and will
+ * receive a CameraCaptureSession (castable to {@link CameraSharedCaptureSession}).</p>
+ *
+ * <p>Shared capture sessions uses a predefined configuration detailed in
+ * {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION}. Using different configuration values
+ * when creating session will result in an {@link IllegalArgumentException}.</p>
+ *
+ * <p>When camera is opened in shared mode, the highest priority client among all the clients will
+ * be the primary client while the others would be secondary clients. Clients will know if they are
+ * primary or secondary by the device state callback
+ * {@link CameraDevice.StateCallback#onOpenedInSharedMode}. Once the camera has been opened in
+ * shared mode, their access priorities of being a primary or secondary client can change if
+ * another higher priority client opens the camera later. Once the camera has been opened,
+ * any change in primary client status will be shared by the device state callback
+ * {@link CameraDevice.StateCallback#onClientSharedAccessPriorityChanged}.</p>
+ *
+ * <p>The priority of client access is determined by considering two factors: its current process
+ * state and its "out of memory" score. Clients operating in the background are assigned a lower
+ * priority. In contrast, clients running in the foreground, along with system-level clients, are
+ * given a higher priority.</p>
+ *
+ * <p>Primary clients can create capture requests, modify any capture parameters and send them to
+ * the capture session for a one-shot capture or as a repeating request using the following apis:
+ * </p>
+ *
+ * <ul>
+ *
+ * <li>{@link CameraSharedCaptureSession#capture}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#captureSingleRequest}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#setRepeatingRequest}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#setSingleRepeatingRequest}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#stopRepeating}</li>
+ *
+ * </ul>
+ *
+ * <p>Secondary clients cannot create a capture request and modify any capture parameters. However,
+ * they can start the camera streaming to desired surface targets using
+ * {@link CameraSharedCaptureSession#startStreaming}, which will apply default parameters. Once the
+ * streaming has successfully started, then they can stop the streaming using
+ * {@link CameraSharedCaptureSession#stopStreaming}.</p>
+ *
+ * <p>The following APIs are not supported in shared capture sessions by either the primary or
+ * secondary client.</p>
+ *
+ * <ul>
+ *
+ * <li>{@link CameraSharedCaptureSession#captureBurst}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#captureBurstRequests}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#setRepeatingBurst}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#setRepeatingBurstRequests}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#switchToOffline}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#updateOutputConfiguration}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#finalizeOutputConfigurations}</li>
+ *
+ * <li>{@link CameraSharedCaptureSession#prepare}</li>
+ *
+ * </ul>
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+@SystemApi
+public abstract class CameraSharedCaptureSession extends CameraCaptureSession {
+
+    /**
+     * Request start of the streaming of camera images by this shared capture session.
+     *
+     * <p>With this method, the camera device will continually capture images
+     * using the settings provided by primary client if there is ongoing repeating request
+     * by the primary client or default settings if no ongoing streaming request in progress.</p>
+     *
+     * <p> startStreaming has lower priority than the capture requests submitted
+     * through {@link #capture} by primary client, so if {@link #capture} is called when a
+     * streaming is active, the capture request will be processed before any further
+     * streaming requests are processed.</p>
+     *
+     * <p>To stop the streaming, call {@link #stopStreaming}</p>
+     *
+     * <p>Calling this method will replace any earlier streaming set up by this method.</p>
+     *
+     * @param surfaces List of target surfaces to use for streaming.
+     * @param executor The executor which will be used for invoking the listener.
+     * @param listener The callback object to notify the status and progress of the image capture.
+     *
+     * @return int A unique capture sequence ID used by
+     *             {@link CaptureCallback#onCaptureSequenceCompleted}.
+     *
+     * @throws CameraAccessException if the camera device is no longer connected or has
+     *                               encountered a fatal error
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
+     * @throws IllegalArgumentException If the request references no surfaces or references surfaces
+     *                                  that are not currently configured as outputs; or
+     *                                  the executor is null, or the listener is null.
+     * @see #stopStreaming
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    @SystemApi
+    public abstract int startStreaming(@NonNull List<Surface> surfaces,
+            @NonNull @CallbackExecutor Executor executor, @NonNull CaptureCallback listener)
+            throws CameraAccessException;
+
+    /**
+     * <p>Cancel any ongoing streaming started by {@link #startStreaming}</p>
+     *
+     * @throws CameraAccessException if the camera device is no longer connected or has
+     *                               encountered a fatal error
+     * @throws IllegalStateException if this session is no longer active, either because the session
+     *                               was explicitly closed, a new session has been created
+     *                               or the camera device has been closed.
+     *
+     * @see #startStreaming
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    @SystemApi
+    public abstract void stopStreaming() throws CameraAccessException;
+}
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 8407258..ea70abb 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.compat.CompatChanges;
@@ -41,12 +42,15 @@
 import android.hardware.camera2.ICameraDeviceUser;
 import android.hardware.camera2.ICameraOfflineSession;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
 import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
 import android.hardware.camera2.params.MultiResolutionStreamInfo;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.params.SessionConfiguration;
+import android.hardware.camera2.params.SharedSessionConfiguration;
+import android.hardware.camera2.params.SharedSessionConfiguration.SharedOutputConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.hardware.camera2.utils.SubmitInfo;
 import android.hardware.camera2.utils.SurfaceUtils;
@@ -188,6 +192,8 @@
 
     private ExecutorService mOfflineSwitchService;
     private CameraOfflineSessionImpl mOfflineSessionImpl;
+    private boolean mSharedMode;
+    private boolean mIsPrimaryClient;
 
     // Runnables for all state transitions, except error, which needs the
     // error code argument
@@ -208,6 +214,25 @@
         }
     };
 
+    private final Runnable mCallOnOpenedInSharedMode = new Runnable() {
+        @Override
+        public void run() {
+            if (!Flags.cameraMultiClient()) {
+                return;
+            }
+            StateCallbackKK sessionCallback = null;
+            synchronized (mInterfaceLock) {
+                if (mRemoteDevice == null) return; // Camera already closed
+
+                sessionCallback = mSessionStateCallback;
+            }
+            if (sessionCallback != null) {
+                sessionCallback.onOpenedInSharedMode(CameraDeviceImpl.this, mIsPrimaryClient);
+            }
+            mDeviceCallback.onOpenedInSharedMode(CameraDeviceImpl.this, mIsPrimaryClient);
+        }
+    };
+
     private final Runnable mCallOnUnconfigured = new Runnable() {
         @Override
         public void run() {
@@ -322,6 +347,32 @@
                 }
             });
         }
+
+        public void onOpenedInSharedMode(@NonNull CameraDevice camera, boolean primaryClient) {
+            if (!Flags.cameraMultiClient()) {
+                return;
+            }
+            mClientExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mClientStateCallback.onOpenedInSharedMode(camera, primaryClient);
+                }
+            });
+        }
+
+        public void onClientSharedAccessPriorityChanged(@NonNull CameraDevice camera,
+                boolean primaryClient) {
+            if (!Flags.cameraMultiClient()) {
+                return;
+            }
+            mClientExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mClientStateCallback.onClientSharedAccessPriorityChanged(camera, primaryClient);
+                }
+            });
+        }
+
         @Override
         public void onOpened(@NonNull CameraDevice camera) {
             mClientExecutor.execute(new Runnable() {
@@ -358,7 +409,8 @@
                         @NonNull CameraManager manager,
                         int appTargetSdkVersion,
                         Context ctx,
-                        @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup) {
+                        @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup,
+                        boolean sharedMode) {
         if (cameraId == null || callback == null || executor == null || characteristics == null
                 || manager == null) {
             throw new IllegalArgumentException("Null argument given");
@@ -375,6 +427,7 @@
         mAppTargetSdkVersion = appTargetSdkVersion;
         mContext = ctx;
         mCameraDeviceSetup = cameraDeviceSetup;
+        mSharedMode = sharedMode;
 
         final int MAX_TAG_LEN = 23;
         String tag = String.format("CameraDevice-JV-%s", mCameraId);
@@ -438,7 +491,12 @@
                 }
             }
 
-            mDeviceExecutor.execute(mCallOnOpened);
+            if (Flags.cameraMultiClient() && mSharedMode) {
+                mIsPrimaryClient = mRemoteDevice.isPrimaryClient();
+                mDeviceExecutor.execute(mCallOnOpenedInSharedMode);
+            } else {
+                mDeviceExecutor.execute(mCallOnOpened);
+            }
             mDeviceExecutor.execute(mCallOnUnconfigured);
 
             mRemoteDeviceInit = true;
@@ -576,7 +634,11 @@
             stopRepeating();
 
             try {
-                waitUntilIdle();
+                // if device is opened in shared mode, there can be multiple clients accessing the
+                // camera device. So do not wait for idle if the device is opened in shared mode.
+                if (!mSharedMode) {
+                    waitUntilIdle();
+                }
 
                 mRemoteDevice.beginConfigure();
 
@@ -764,6 +826,54 @@
                 checkAndWrapHandler(handler), operatingMode, /*sessionParams*/ null);
     }
 
+    private boolean checkSharedOutputConfiguration(OutputConfiguration outConfig) {
+        if (!Flags.cameraMultiClient()) {
+            return false;
+        }
+        SharedSessionConfiguration sharedSessionConfiguration =
+                mCharacteristics.get(CameraCharacteristics.SHARED_SESSION_CONFIGURATION);
+        if (sharedSessionConfiguration == null) {
+            return false;
+        }
+
+        List<SharedOutputConfiguration> sharedConfigs =
+                sharedSessionConfiguration.getOutputStreamsInformation();
+        for (SharedOutputConfiguration sharedConfig : sharedConfigs) {
+            if (outConfig.getConfiguredSize().equals(sharedConfig.getSize())
+                    && (outConfig.getConfiguredFormat() == sharedConfig.getFormat())
+                    && (outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE)
+                    && (outConfig.getSurfaceType() == sharedConfig.getSurfaceType())
+                    && (outConfig.getMirrorMode() == sharedConfig.getMirrorMode())
+                    && (outConfig.getUsage() == sharedConfig.getUsage())
+                    && (outConfig.isReadoutTimestampEnabled()
+                    == sharedConfig.isReadoutTimestampEnabled())
+                    && (outConfig.getTimestampBase() == sharedConfig.getTimestampBase())
+                    && (outConfig.getStreamUseCase() == sharedConfig.getStreamUseCase())
+                    && (outConfig.getColorSpace().equals(
+                    sharedSessionConfiguration.getColorSpace()))
+                    && (outConfig.getDynamicRangeProfile()
+                    == DynamicRangeProfiles.STANDARD)
+                    && (outConfig.getConfiguredDataspace() == sharedConfig.getDataspace())
+                    && (Objects.equals(outConfig.getPhysicalCameraId(),
+                    sharedConfig.getPhysicalCameraId()))
+                    && (outConfig.getSensorPixelModes().isEmpty())
+                    && (!outConfig.isShared())) {
+                //Found valid config, return true
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkSharedSessionConfiguration(List<OutputConfiguration> outputConfigs) {
+        for (OutputConfiguration out : outputConfigs) {
+            if (!checkSharedOutputConfiguration(out)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     @Override
     public void createCaptureSession(SessionConfiguration config)
             throws CameraAccessException {
@@ -778,6 +888,14 @@
         if (config.getExecutor() == null) {
             throw new IllegalArgumentException("Invalid executor");
         }
+        if (mSharedMode) {
+            if (config.getSessionType() != SessionConfiguration.SESSION_SHARED) {
+                throw new IllegalArgumentException("Invalid session type");
+            }
+            if (!checkSharedSessionConfiguration(outputConfigs)) {
+                throw new IllegalArgumentException("Invalid output configurations");
+            }
+        }
         createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs,
                 config.getStateCallback(), config.getExecutor(), config.getSessionType(),
                 config.getSessionParameters());
@@ -801,6 +919,11 @@
                 throw new IllegalArgumentException("Constrained high speed session doesn't support"
                         + " input configuration yet.");
             }
+            boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE);
+            if (isSharedSession && inputConfig != null) {
+                throw new IllegalArgumentException("Shared capture session doesn't support"
+                        + " input configuration yet.");
+            }
 
             if (mCurrentExtensionSession != null) {
                 mCurrentExtensionSession.commitStats();
@@ -860,6 +983,10 @@
                 newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++,
                         callback, executor, this, mDeviceExecutor, configureSuccess,
                         mCharacteristics);
+            } else if (isSharedSession) {
+                newSession = new CameraSharedCaptureSessionImpl(mNextSessionId++,
+                        callback, executor, this, mDeviceExecutor, configureSuccess,
+                        mIsPrimaryClient);
             } else {
                 newSession = new CameraCaptureSessionImpl(mNextSessionId++, input,
                         callback, executor, this, mDeviceExecutor, configureSuccess);
@@ -1882,6 +2009,40 @@
         }
     }
 
+    /**
+     * Callback when client access priorities change when camera is opened in shared mode.
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    public void onClientSharedAccessPriorityChanged(boolean primaryClient) {
+        if (DEBUG) {
+            Log.d(TAG, String.format(
+                    "onClientSharedAccessPriorityChanged received, primary client = "
+                    + primaryClient));
+        }
+        synchronized (mInterfaceLock) {
+            if (mRemoteDevice == null && mRemoteDeviceInit) {
+                return; // Camera already closed, user is not interested in this callback anymore.
+            }
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mDeviceExecutor.execute(obtainRunnable(
+                        CameraDeviceImpl::notifyClientSharedAccessPriorityChanged, this,
+                        primaryClient).recycleOnUse());
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+    }
+
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    private void notifyClientSharedAccessPriorityChanged(boolean primaryClient) {
+        if (!CameraDeviceImpl.this.isClosed()) {
+            mIsPrimaryClient = primaryClient;
+            mDeviceCallback.onClientSharedAccessPriorityChanged(CameraDeviceImpl.this,
+                    primaryClient);
+        }
+    }
+
     public void onDeviceError(final int errorCode, CaptureResultExtras resultExtras) {
         if (DEBUG) {
             Log.d(TAG, String.format(
@@ -2447,6 +2608,12 @@
         }
 
         @Override
+        @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+        public void onClientSharedAccessPriorityChanged(boolean primaryClient) {
+            CameraDeviceImpl.this.onClientSharedAccessPriorityChanged(primaryClient);
+        }
+
+        @Override
         public void onPrepared(int streamId) {
             final OutputConfiguration output;
             final StateCallbackKK sessionCallback;
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 1cc0856..c0a5928 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -63,6 +63,7 @@
 import android.hardware.camera2.params.RecommendedStreamConfiguration;
 import android.hardware.camera2.params.RecommendedStreamConfigurationMap;
 import android.hardware.camera2.params.ReprocessFormatsMap;
+import android.hardware.camera2.params.SharedSessionConfiguration;
 import android.hardware.camera2.params.StreamConfiguration;
 import android.hardware.camera2.params.StreamConfigurationDuration;
 import android.hardware.camera2.params.StreamConfigurationMap;
@@ -866,6 +867,15 @@
                         return (T) metadata.getLensIntrinsicSamples();
                     }
                 });
+        sGetCommandMap.put(
+                CameraCharacteristics.SHARED_SESSION_CONFIGURATION.getNativeKey(),
+                        new GetCommand() {
+                    @Override
+                    @SuppressWarnings("unchecked")
+                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
+                        return (T) metadata.getSharedSessionConfiguration();
+                    }
+                });
     }
 
     private int[] getAvailableFormats() {
@@ -1658,6 +1668,22 @@
                 listHighResolution);
     }
 
+    private SharedSessionConfiguration getSharedSessionConfiguration() {
+        if (!Flags.cameraMultiClient()) {
+            return null;
+        }
+        Integer sharedSessionColorSpace = getBase(
+                CameraCharacteristics.SHARED_SESSION_COLOR_SPACE);
+        long[] sharedOutputConfigurations = getBase(
+                CameraCharacteristics.SHARED_SESSION_OUTPUT_CONFIGURATIONS);
+
+        if ((sharedSessionColorSpace == null) || (sharedOutputConfigurations == null)) {
+            return null;
+        }
+
+        return new SharedSessionConfiguration(sharedSessionColorSpace, sharedOutputConfigurations);
+    }
+
     private StreamConfigurationMap getStreamConfigurationMapMaximumResolution() {
         StreamConfiguration[] configurations = getBase(
                 CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
diff --git a/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java
index eb2ff88..1769c46 100644
--- a/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java
@@ -16,11 +16,13 @@
 
 package android.hardware.camera2.impl;
 
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.FlaggedApi;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
 import android.hardware.camera2.CameraOfflineSession;
 import android.hardware.camera2.CameraOfflineSession.CameraOfflineSessionCallback;
 import android.hardware.camera2.CaptureFailure;
@@ -40,15 +42,15 @@
 import android.util.SparseArray;
 import android.view.Surface;
 
+import com.android.internal.camera.flags.Flags;
+
 import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.Executor;
-
-import static com.android.internal.util.Preconditions.*;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class CameraOfflineSessionImpl extends CameraOfflineSession
         implements IBinder.DeathRecipient {
@@ -176,6 +178,12 @@
         }
 
         @Override
+        @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+        public void onClientSharedAccessPriorityChanged(boolean primaryClient) {
+            Log.v(TAG, "onClientSharedAccessPriorityChanged primaryClient = " + primaryClient);
+        }
+
+        @Override
         public void onDeviceIdle() {
             synchronized(mInterfaceLock) {
                 if (mRemoteSession == null) {
diff --git a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java
new file mode 100644
index 0000000..a1f31c0
--- /dev/null
+++ b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java
@@ -0,0 +1,242 @@
+/*
+ * 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.hardware.camera2.impl;
+
+import android.annotation.FlaggedApi;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraSharedCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.view.Surface;
+
+import com.android.internal.camera.flags.Flags;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Standard implementation of CameraSharedCaptureSession.
+ *
+ * <p>
+ * Mostly just forwards calls to an instance of CameraCaptureSessionImpl,
+ * but implements the few necessary behavior changes and additional methods required
+ * for the shared session mode.
+ * </p>
+ */
+@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+public class CameraSharedCaptureSessionImpl
+        extends CameraSharedCaptureSession implements CameraCaptureSessionCore {
+    private static final String TAG = "CameraSharedCaptureSessionImpl";
+    private final CameraCaptureSessionImpl mSessionImpl;
+    private final ConditionVariable mInitialized = new ConditionVariable();
+    private boolean mIsPrimary;
+
+    /**
+     * Create a new CameraCaptureSession.
+     */
+    CameraSharedCaptureSessionImpl(int id,
+            CameraCaptureSession.StateCallback callback, Executor stateExecutor,
+            android.hardware.camera2.impl.CameraDeviceImpl deviceImpl,
+            Executor deviceStateExecutor, boolean configureSuccess, boolean isPrimary) {
+        CameraCaptureSession.StateCallback wrapperCallback = new WrapperCallback(callback);
+        mSessionImpl = new CameraCaptureSessionImpl(id, /*input*/null, wrapperCallback,
+                stateExecutor, deviceImpl, deviceStateExecutor, configureSuccess);
+        mIsPrimary = isPrimary;
+        mInitialized.open();
+    }
+
+    @Override
+    public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback listener)
+            throws CameraAccessException {
+        // Todo: Need to add implementation.
+        return 0;
+    }
+
+    @Override
+    public void stopStreaming() throws CameraAccessException {
+      // Todo: Need to add implementation.
+    }
+
+    @Override
+    public void close() {
+        mSessionImpl.close();
+    }
+
+    @Override
+    public Surface getInputSurface() {
+        return null;
+    }
+
+    @Override
+    public boolean isReprocessable() {
+        return false;
+    }
+
+    @Override
+    public void abortCaptures() throws CameraAccessException {
+        if (mIsPrimary) {
+            mSessionImpl.abortCaptures();
+        }
+    }
+
+    @Override
+    public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener,
+            Handler handler) throws CameraAccessException {
+        if (mIsPrimary) {
+            return mSessionImpl.setRepeatingRequest(request, listener, handler);
+        }
+        throw new UnsupportedOperationException("Shared capture session only supports this method"
+                + " for primary clients");
+    }
+
+    @Override
+    public void stopRepeating() throws CameraAccessException {
+        if (mIsPrimary) {
+            mSessionImpl.stopRepeating();
+        }
+    }
+
+    @Override
+    public int capture(CaptureRequest request, CaptureCallback listener, Handler handler)
+            throws CameraAccessException {
+        if (mIsPrimary) {
+            return mSessionImpl.capture(request, listener, handler);
+        }
+        throw new UnsupportedOperationException("Shared capture session only supports this method"
+                + " for primary clients");
+    }
+
+    @Override
+    public void tearDown(Surface surface) throws CameraAccessException {
+        mSessionImpl.tearDown(surface);
+    }
+
+    @Override
+    public CameraDevice getDevice() {
+        return mSessionImpl.getDevice();
+    }
+
+    @Override
+    public boolean isAborting() {
+        return mSessionImpl.isAborting();
+    }
+
+    @Override
+    public CameraDeviceImpl.StateCallbackKK getDeviceStateCallback() {
+        return mSessionImpl.getDeviceStateCallback();
+    }
+
+    @Override
+    public void replaceSessionClose() {
+        mSessionImpl.replaceSessionClose();
+    }
+
+    @Override
+    public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener,
+            Handler handler) throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared Capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener,
+            Handler handler) throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared Capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public void updateOutputConfiguration(OutputConfiguration config)
+            throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public void finalizeOutputConfigurations(List<OutputConfiguration> deferredOutputConfigs)
+            throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public void prepare(Surface surface) throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public void prepare(int maxCount, Surface surface) throws CameraAccessException {
+        throw new UnsupportedOperationException("Shared capture session doesn't support"
+                + " this method");
+    }
+
+    @Override
+    public void closeWithoutDraining() {
+        throw new UnsupportedOperationException("Shared capture session doesn't support"
+                + " this method");
+    }
+
+    private class WrapperCallback extends StateCallback {
+        private final StateCallback mCallback;
+
+        WrapperCallback(StateCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            mInitialized.block();
+            mCallback.onConfigured(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            mInitialized.block();
+            mCallback.onConfigureFailed(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            mCallback.onReady(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            mCallback.onActive(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+            mCallback.onCaptureQueueEmpty(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            mCallback.onClosed(CameraSharedCaptureSessionImpl.this);
+        }
+
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+            mCallback.onSurfacePrepared(CameraSharedCaptureSessionImpl.this,
+                    surface);
+        }
+    }
+}
diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
index aec2cff..831c75ec 100644
--- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
+++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
@@ -301,4 +301,16 @@
         }
     }
 
+    /**
+     * API to check if the client is primary client when camera device is opened in shared mode.
+     */
+    public boolean isPrimaryClient() throws CameraAccessException {
+        try {
+            return mRemoteDevice.isPrimaryClient();
+        } catch (ServiceSpecificException e) {
+            throw ExceptionUtils.throwAsPublicException(e);
+        } catch (RemoteException e) {
+            throw ExceptionUtils.throwAsPublicException(e);
+        }
+    }
 }
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index d38be9b..e12c463 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -29,6 +29,7 @@
 import android.graphics.ColorSpace;
 import android.graphics.ImageFormat;
 import android.graphics.ImageFormat.Format;
+import android.hardware.DataSpace.NamedDataSpace;
 import android.hardware.HardwareBuffer;
 import android.hardware.HardwareBuffer.Usage;
 import android.hardware.camera2.CameraCaptureSession;
@@ -1729,6 +1730,79 @@
     }
 
     /**
+     * Get the configured format associated with this {@link OutputConfiguration}.
+     *
+     * @return {@link android.graphics.ImageFormat#Format} associated with this
+     *         {@link OutputConfiguration}.
+     *
+     * @hide
+     */
+    public @Format int getConfiguredFormat() {
+        return mConfiguredFormat;
+    }
+
+    /**
+     * Get the usage flag associated with this {@link OutputConfiguration}.
+     *
+     * @return {@link HardwareBuffer#Usage} associated with this {@link OutputConfiguration}.
+     *
+     * @hide
+     */
+    public @Usage long getUsage() {
+        return mUsage;
+    }
+
+    /**
+     * Get the surface type associated with this {@link OutputConfiguration}.
+     *
+     * @return The surface type associated with this {@link OutputConfiguration}.
+     *
+     * @see #SURFACE_TYPE_SURFACE_VIEW
+     * @see #SURFACE_TYPE_SURFACE_TEXTURE
+     * @see #SURFACE_TYPE_MEDIA_RECORDER
+     * @see #SURFACE_TYPE_MEDIA_CODEC
+     * @see #SURFACE_TYPE_IMAGE_READER
+     * @see #SURFACE_TYPE_UNKNOWN
+     * @hide
+     */
+    public int getSurfaceType() {
+        return mSurfaceType;
+    }
+
+    /**
+     * Get the sensor pixel modes associated with this {@link OutputConfiguration}.
+     *
+     * @return List of {@link #SensorPixelMode} associated with this {@link OutputConfiguration}.
+     *
+     * @hide
+     */
+    public @NonNull List<Integer> getSensorPixelModes() {
+        return mSensorPixelModesUsed;
+    }
+
+     /**
+     * Get the sharing mode associated with this {@link OutputConfiguration}.
+     *
+     * @return true if surface sharing is enabled with this {@link OutputConfiguration}.
+     *
+     * @hide
+     */
+    public boolean isShared() {
+        return mIsShared;
+    }
+
+    /**
+     * Get the dataspace associated with this {@link OutputConfiguration}.
+     *
+     * @return {@link Dataspace#NamedDataSpace} for this {@link OutputConfiguration}.
+     *
+     * @hide
+     */
+    public @NamedDataSpace int getConfiguredDataspace() {
+        return mConfiguredDataspace;
+    }
+
+    /**
      * Get the physical camera ID associated with this {@link OutputConfiguration}.
      *
      * <p>If this OutputConfiguration isn't targeting a physical camera of a logical
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index 50c6b5b..82aa64b 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -23,6 +23,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.graphics.ColorSpace;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
@@ -78,6 +79,19 @@
         CameraDevice.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED;
 
     /**
+     * A shared session type containing instances of {@link OutputConfiguration} from a set of
+     * predefined stream configurations. A shared session can be shared among multiple clients.
+     * Shared session does not have any {@link InputConfiguration} as it does not support
+     * reprocessable sessions.
+     *
+     * @see CameraDevice#createCaptureSession(SessionConfiguration)
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+    @SystemApi
+    public static final int SESSION_SHARED = CameraDevice.SESSION_OPERATION_MODE_SHARED;
+
+    /**
      * First vendor-specific session mode
      * @hide
      */
diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java
new file mode 100644
index 0000000..cdcc92c
--- /dev/null
+++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java
@@ -0,0 +1,312 @@
+/*
+ * 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.hardware.camera2.params;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.graphics.ColorSpace;
+import android.graphics.ImageFormat.Format;
+import android.hardware.DataSpace.NamedDataSpace;
+import android.hardware.HardwareBuffer.Usage;
+import android.hardware.camera2.params.OutputConfiguration.MirrorMode;
+import android.hardware.camera2.params.OutputConfiguration.StreamUseCase;
+import android.hardware.camera2.params.OutputConfiguration.TimestampBase;
+import android.util.Log;
+import android.util.Size;
+
+import com.android.internal.camera.flags.Flags;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Immutable class to store the shared session configuration
+ * {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION} to set up
+ * {@link android.view.Surface Surfaces} for creating a
+ * {@link android.hardware.camera2.CameraSharedCaptureSession capture session} using
+ * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)} and
+ * {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED
+ * shared capture session} when camera has been opened in shared mode using
+ * {@link #openSharedCamera(String, Executor, StateCallback)}.
+ *
+ * <p>This is the authoritative list for all output configurations that are supported by a camera
+ * device when opened in shared mode.</p>
+ *
+ * <p>An instance of this object is available from {@link CameraCharacteristics} using
+ * the {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION} key and the
+ * {@link CameraCharacteristics#get} method.</p>
+ *
+ * <pre><code>{@code
+ * CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
+ * StreamConfigurationMap configs = characteristics.get(
+ *         CameraCharacteristics.SHARED_SESSION_CONFIGURATION);
+ * }</code></pre>
+ *
+ * @see CameraCharacteristics#SHARED_SESSION_CONFIGURATION
+ * @see CameraDevice#createCaptureSession(SessionConfiguration)
+ * @see SessionConfiguration#SESSION_SHARED
+ * @see CameraManager#openSharedCamera(String, Executor, StateCallback)
+ *
+ *  @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT)
+public final class SharedSessionConfiguration {
+    private static final String TAG = "SharedSessionConfiguration";
+    // Metadata android.info.availableSharedOutputConfigurations has list of shared output
+    // configurations. Each output configuration has minimum of 11 entries of size long
+    // followed by the physical camera id if present.
+    // See android.info.availableSharedOutputConfigurations for details.
+    private static final int SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES = 11;
+    /**
+     * Immutable class to store shared output stream information.
+     */
+    public static final class SharedOutputConfiguration {
+        private final int mSurfaceType;
+        private final Size mSize;
+        private final int mFormat;
+        private final int mDataspace;
+        private final long mStreamUseCase;
+        private String mPhysicalCameraId;
+        private final long mUsage;
+        private int mTimestampBase;
+        private int mMirrorMode;
+        private boolean mReadoutTimestampEnabled;
+
+        /**
+         * Create a new {@link SharedOutputConfiguration}.
+         *
+         * @param surfaceType Surface Type for this output configuration.
+         * @param sz Size for this output configuration.
+         * @param format {@link android.graphics.ImageFormat#Format} associated with this
+         *         {@link OutputConfiguration}.
+         * @param mirrorMode  {@link OutputConfiguration#MirrorMode} for this output configuration.
+         * @param readoutTimeStampEnabled  Flag indicating whether readout timestamp is enabled
+         *         for this output configuration.
+         * @param timestampBase {@link OutputConfiguration#TimestampBase} for this output
+         *         configuration.
+         * @param dataspace {@link Dataspace#NamedDataSpace} for this output configuration.
+         * @param usage   {@link HardwareBuffer#Usage} for this output configuration.
+         * @param streamUseCase {@link OutputConfiguration#StreamUseCase} for this output
+         *         configuration.
+         * @param physicalCamId Physical Camera Id for this output configuration.
+         *
+         * @hide
+         */
+        public SharedOutputConfiguration(int surfaceType, @NonNull Size sz, @Format int format,
+                @MirrorMode int mirrorMode, boolean readoutTimeStampEnabled,
+                @TimestampBase int timestampBase, @NamedDataSpace int dataspace, @Usage long usage,
+                @StreamUseCase long streamUseCase, @Nullable String physicalCamId) {
+            mSurfaceType = surfaceType;
+            mSize = sz;
+            mFormat = format;
+            mMirrorMode = mirrorMode;
+            mReadoutTimestampEnabled = readoutTimeStampEnabled;
+            mTimestampBase = timestampBase;
+            mDataspace = dataspace;
+            mUsage = usage;
+            mStreamUseCase = streamUseCase;
+            mPhysicalCameraId = physicalCamId;
+        }
+
+        /**
+         * Returns the surface type configured for the shared output configuration.
+         * @return  SURFACE_TYPE_UNKNOWN = -1
+         *          SURFACE_TYPE_SURFACE_VIEW = 0
+         *          SURFACE_TYPE_SURFACE_TEXTURE = 1
+         *          SURFACE_TYPE_MEDIA_RECORDER = 2
+         *          SURFACE_TYPE_MEDIA_CODEC = 3
+         *          SURFACE_TYPE_IMAGE_READER = 4
+         */
+        public int getSurfaceType() {
+            return mSurfaceType;
+        }
+
+        /**
+         * Returns the format of the shared output configuration.
+         * @return format The format of the configured output. This must be one of the
+         *     {@link android.graphics.ImageFormat} or {@link android.graphics.PixelFormat}
+         *     constants. Note that not all formats are supported by the camera device.
+         */
+        public @Format int getFormat() {
+            return mFormat;
+        }
+
+        /**
+         * Returns the configured size for the shared output configuration.
+         * @return surfaceSize Size for the shared output configuration
+         *
+         */
+        public @NonNull Size getSize() {
+            return mSize;
+        }
+
+        /**
+         * Return datatspace configured for the shared output configuration.
+         *
+         * @return {@link Dataspace#NamedDataSpace} configured for shared session
+         */
+        public @NamedDataSpace int getDataspace() {
+            return mDataspace;
+        }
+
+        /**
+         * Get the  mirroring mode configured for the shared output configuration.
+         *
+         * @return {@link OutputConfiguration#MirrorMode} configured for the shared session
+         */
+        public @MirrorMode int getMirrorMode() {
+            return mMirrorMode;
+        }
+
+        /**
+         * Get the stream use case configured for the shared output configuration.
+         *
+         * @return {@link OutputConfiguration#StreamUseCase} configured for the shared session
+         */
+        public @StreamUseCase long getStreamUseCase() {
+            return mStreamUseCase;
+        }
+
+        /**
+         * Get the timestamp base configured for the shared output configuration.
+         *
+         * @return {@link OutputConfiguration#TimestampBase} configured for the shared session
+         */
+        public @TimestampBase int getTimestampBase() {
+            return mTimestampBase;
+        }
+
+        /** Whether readout timestamp is used for this shared output configuration.
+         *
+         */
+        public boolean isReadoutTimestampEnabled() {
+            return mReadoutTimestampEnabled;
+        }
+
+        /** Returns the usage if set for this shared output configuration.
+         *
+         * @return {@link HardwareBuffer#Usage} flags if set for shared output configuration with
+         *         the ImageReader output surface.
+         */
+        public @Usage long getUsage() {
+            return mUsage;
+        }
+
+        public @Nullable String getPhysicalCameraId() {
+            return mPhysicalCameraId;
+        }
+    }
+
+    /**
+     * Create a new {@link SharedSessionConfiguration}.
+     *
+     * <p>The array parameters ownership is passed to this object after creation; do not
+     * write to them after this constructor is invoked.</p>
+     *
+     * @param sharedColorSpace the colorspace to be used for the shared output configurations.
+     * @param sharedOutputConfigurations a non-{@code null} array of metadata
+     *                                  android.info.availableSharedOutputConfigurations
+     *
+     * @hide
+     */
+    public SharedSessionConfiguration(int sharedColorSpace,
+            @NonNull long[] sharedOutputConfigurations) {
+        mColorSpace = sharedColorSpace;
+        byte physicalCameraIdLen;
+        int surfaceType, width, height, format, mirrorMode, timestampBase, dataspace;
+        long usage, streamUseCase;
+        boolean isReadOutTimestampEnabled;
+        // Parse metadata android.info.availableSharedOutputConfigurations which contains
+        // list of shared output configurations.
+        int numOfEntries = sharedOutputConfigurations.length;
+        int i = 0;
+        while (numOfEntries >= SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES) {
+            surfaceType = (int) sharedOutputConfigurations[i];
+            width = (int) sharedOutputConfigurations[i + 1];
+            height = (int) sharedOutputConfigurations[i + 2];
+            format = (int) sharedOutputConfigurations[i + 3];
+            mirrorMode = (int) sharedOutputConfigurations[i + 4];
+            isReadOutTimestampEnabled = (sharedOutputConfigurations[i + 5] != 0);
+            timestampBase = (int) sharedOutputConfigurations[i + 6];
+            dataspace = (int) sharedOutputConfigurations[i + 7];
+            usage = sharedOutputConfigurations[i + 8];
+            streamUseCase = sharedOutputConfigurations[i + 9];
+            physicalCameraIdLen = (byte) sharedOutputConfigurations[i + 10];
+            numOfEntries -= SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES;
+            i += SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES;
+            if (numOfEntries < physicalCameraIdLen) {
+                Log.e(TAG, "Number of remaining data in shared configuration is less than"
+                        + " physical camera id length . Malformed metadata"
+                        + " android.info.availableSharedOutputConfigurations.");
+                break;
+            }
+            StringBuilder physicalCameraId =  new StringBuilder();
+            long asciiValue;
+            for (int j = 0; j < physicalCameraIdLen; j++) {
+                asciiValue = sharedOutputConfigurations[i + j];
+                if (asciiValue == 0) { // Check for null terminator
+                    break;
+                }
+                physicalCameraId.append((char) asciiValue);
+            }
+            SharedOutputConfiguration outputInfo;
+            outputInfo = new SharedOutputConfiguration(surfaceType, new Size(width, height),
+                    format,  mirrorMode, isReadOutTimestampEnabled, timestampBase,
+                    dataspace, usage, streamUseCase, physicalCameraId.toString());
+            mOutputStreamConfigurations.add(outputInfo);
+            i += physicalCameraIdLen;
+            numOfEntries -= physicalCameraIdLen;
+        }
+        if (numOfEntries != 0) {
+            Log.e(TAG, "Unexpected entries left in shared output configuration."
+                    + " Malformed metadata android.info.availableSharedOutputConfigurations.");
+        }
+    }
+
+    /**
+     * Return the shared session color space which is configured.
+     *
+     * @return the shared session color space
+     */
+    @SuppressLint("MethodNameUnits")
+    public @Nullable ColorSpace getColorSpace() {
+        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
+        } else {
+            return null;
+        }
+    }
+    /**
+     * Get information about each shared output configuarion in the shared session.
+     *
+     * @return Non-modifiable list of output configuration.
+     *
+     */
+    public @NonNull List<SharedOutputConfiguration> getOutputStreamsInformation() {
+        return Collections.unmodifiableList(mOutputStreamConfigurations);
+    }
+
+    private int mColorSpace;
+    private final ArrayList<SharedOutputConfiguration> mOutputStreamConfigurations =
+            new ArrayList<SharedOutputConfiguration>();
+}
+
diff --git a/core/java/android/hardware/contexthub/HubDiscoveryInfo.java b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java
new file mode 100644
index 0000000..875c4b4
--- /dev/null
+++ b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+import android.hardware.location.ContextHubManager;
+
+/**
+ * Class that represents the result of from an hub endpoint discovery.
+ *
+ * <p>The type is returned from an endpoint discovery query via {@link
+ * ContextHubManager#findEndpoints}. Application may use the values {@link #getHubEndpointInfo} to
+ * retrieve the {@link HubEndpointInfo} that describes the endpoint that matches the query. The
+ * class provides flexibility in returning more information (e.g. service provided by the endpoint)
+ * in addition to the information about the endpoint.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public class HubDiscoveryInfo {
+    // TODO(b/375487784): Add ServiceInfo to the result.
+    android.hardware.contexthub.HubEndpointInfo mEndpointInfo;
+
+    /**
+     * Constructor for internal use.
+     *
+     * @hide
+     */
+    public HubDiscoveryInfo(android.hardware.contexthub.HubEndpointInfo endpointInfo) {
+        mEndpointInfo = endpointInfo;
+    }
+
+    /** Get the {@link android.hardware.contexthub.HubEndpointInfo} for the endpoint found. */
+    @NonNull
+    public HubEndpointInfo getHubEndpointInfo() {
+        return mEndpointInfo;
+    }
+}
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl
similarity index 60%
copy from core/java/android/text/ClientFlags.java
copy to core/java/android/hardware/contexthub/HubEndpointInfo.aidl
index ca88764..025b2b1 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,7 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.hardware.contexthub;
 
-/**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
- * @hide
- */
-public class ClientFlags {
-}
+/** @hide */
+parcelable HubEndpointInfo;
diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.java b/core/java/android/hardware/contexthub/HubEndpointInfo.java
new file mode 100644
index 0000000..c17fc00
--- /dev/null
+++ b/core/java/android/hardware/contexthub/HubEndpointInfo.java
@@ -0,0 +1,189 @@
+/*
+ * 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.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Parcelable representing an endpoint from ContextHub or VendorHub.
+ *
+ * <p>HubEndpointInfo contains information about an endpoint, including its name, tag and other
+ * information. A HubEndpointInfo object can be used to accurately identify a specific endpoint.
+ * Application can use this object to identify and describe an endpoint.
+ *
+ * <p>See: {@link android.hardware.location.ContextHubManager#findEndpoints} for how to retrieve
+ * {@link HubEndpointInfo} for endpoints on a hub.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public final class HubEndpointInfo implements Parcelable {
+    /**
+     * A unique identifier for one endpoint. A unique identifier for one endpoint consists of two
+     * parts: (1) a unique long number for a hub and (2) a long number for the endpoint, unique
+     * within a hub. This class overrides equality methods and can be used to compare if two
+     * endpoints are the same.
+     */
+    public static class HubEndpointIdentifier {
+        private final long mEndpointId;
+        private final long mHubId;
+
+        /** @hide */
+        public HubEndpointIdentifier(long hubId, long endpointId) {
+            mEndpointId = endpointId;
+            mHubId = hubId;
+        }
+
+        /** @hide */
+        public HubEndpointIdentifier(android.hardware.contexthub.EndpointId halEndpointId) {
+            mEndpointId = halEndpointId.id;
+            mHubId = halEndpointId.hubId;
+        }
+
+        /** Get the endpoint portion of the identifier. */
+        public long getEndpoint() {
+            return mEndpointId;
+        }
+
+        /** Get the hub portion of the identifier. */
+        public long getHub() {
+            return mHubId;
+        }
+
+        /**
+         * Create an invalid endpoint id, to represent endpoint that are not yet registered with the
+         * HAL.
+         *
+         * @hide
+         */
+        public static HubEndpointIdentifier invalid() {
+            return new HubEndpointIdentifier(
+                    android.hardware.contexthub.HubInfo.HUB_ID_INVALID,
+                    android.hardware.contexthub.EndpointId.ENDPOINT_ID_INVALID);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mEndpointId, mHubId);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof HubEndpointIdentifier other)) {
+                return false;
+            }
+            if (other.mHubId != mHubId) {
+                return false;
+            }
+            return other.mEndpointId == mEndpointId;
+        }
+    }
+
+    private final HubEndpointIdentifier mId;
+    private final String mName;
+    @Nullable private final String mTag;
+
+    // TODO(b/375487784): Add Service/version and other information to this object
+
+    /** @hide */
+    public HubEndpointInfo(android.hardware.contexthub.EndpointInfo endpointInfo) {
+        mId = new HubEndpointIdentifier(endpointInfo.id.hubId, endpointInfo.id.id);
+        mName = endpointInfo.name;
+        mTag = endpointInfo.tag;
+    }
+
+    private HubEndpointInfo(Parcel in) {
+        long hubId = in.readLong();
+        long endpointId = in.readLong();
+        mName = in.readString();
+        mTag = in.readString();
+
+        mId = new HubEndpointIdentifier(hubId, endpointId);
+    }
+
+    /** Parcel implementation details */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Parcel implementation details */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mId.getHub());
+        dest.writeLong(mId.getEndpoint());
+        dest.writeString(mName);
+        dest.writeString(mTag);
+    }
+
+    /** Get a unique identifier for this endpoint. */
+    @NonNull
+    public HubEndpointIdentifier getIdentifier() {
+        return mId;
+    }
+
+    /** Get the human-readable name of this endpoint (for debugging purposes). */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Get the tag that further identifies the submodule that created this endpoint. For example, a
+     * single application could provide multiple endpoints. These endpoints will share the same
+     * name, but will have different tags. This tag can be used to identify the submodule within the
+     * application that provided the endpoint.
+     */
+    @Nullable
+    public String getTag() {
+        return mTag;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder();
+        out.append("Endpoint [0x");
+        out.append(Long.toHexString(mId.getEndpoint()));
+        out.append("@ Hub 0x");
+        out.append(Long.toHexString(mId.getHub()));
+        out.append("] Name=");
+        out.append(mName);
+        out.append(", Tag=");
+        out.append(mTag);
+        return out.toString();
+    }
+
+    public static final @android.annotation.NonNull Creator<HubEndpointInfo> CREATOR =
+            new Creator<>() {
+                public HubEndpointInfo createFromParcel(Parcel in) {
+                    return new HubEndpointInfo(in);
+                }
+
+                public HubEndpointInfo[] newArray(int size) {
+                    return new HubEndpointInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/contexthub/OWNERS b/core/java/android/hardware/contexthub/OWNERS
new file mode 100644
index 0000000..a65a2bf
--- /dev/null
+++ b/core/java/android/hardware/contexthub/OWNERS
@@ -0,0 +1,2 @@
+# ContextHub team
+file:platform/system/chre:/OWNERS
diff --git a/core/java/android/hardware/flags/overlayproperties_flags.aconfig b/core/java/android/hardware/flags/flags.aconfig
similarity index 63%
rename from core/java/android/hardware/flags/overlayproperties_flags.aconfig
rename to core/java/android/hardware/flags/flags.aconfig
index 6c86108..5ca6c6b 100644
--- a/core/java/android/hardware/flags/overlayproperties_flags.aconfig
+++ b/core/java/android/hardware/flags/flags.aconfig
@@ -2,6 +2,15 @@
 container: "system"
 
 flag {
+    name: "luts_api"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "core_graphics"
+    description: "public Luts related Apis"
+    bug: "349667978"
+}
+
+flag {
     name: "overlayproperties_class_api"
     is_exported: true
     namespace: "core_graphics"
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 4b2f2c2..fee0749 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -170,4 +170,11 @@
   namespace: "input"
   description: "Adds key gestures for talkback and magnifier"
   bug: "375277034"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "can_window_override_power_gesture_api"
+  namespace: "wallet_integration"
+  description: "Adds new API in WindowManager class to check if the window can override the power key double tap behavior."
+  bug: "378736024"
+  }
\ No newline at end of file
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 494bfc9..e009c2f 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -34,6 +34,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubDiscoveryInfo;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.Looper;
@@ -42,6 +44,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -679,6 +682,29 @@
     }
 
     /**
+     * Find a list of endpoints that matches a specific ID.
+     *
+     * @param endpointId Statically generated ID for an endpoint.
+     * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
+     */
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @NonNull
+    public List<HubDiscoveryInfo> findEndpoints(long endpointId) {
+        try {
+            List<HubEndpointInfo> endpointInfos = mService.findEndpoints(endpointId);
+            List<HubDiscoveryInfo> results = new ArrayList<>(endpointInfos.size());
+            // Wrap with result type
+            for (HubEndpointInfo endpointInfo : endpointInfos) {
+                results.add(new HubDiscoveryInfo(endpointInfo));
+            }
+            return results;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Set a callback to receive messages from the context hub
      *
      * @param callback Callback object
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index b0cc763..fc6a70a 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -18,17 +18,18 @@
 
 // Declare any non-default types here with import statements
 import android.app.PendingIntent;
-import android.hardware.location.HubInfo;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
-import android.hardware.location.NanoApp;
-import android.hardware.location.NanoAppBinary;
-import android.hardware.location.NanoAppFilter;
-import android.hardware.location.NanoAppInstanceInfo;
+import android.hardware.location.HubInfo;
 import android.hardware.location.IContextHubCallback;
 import android.hardware.location.IContextHubClient;
 import android.hardware.location.IContextHubClientCallback;
 import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoApp;
+import android.hardware.location.NanoAppBinary;
+import android.hardware.location.NanoAppFilter;
+import android.hardware.location.NanoAppInstanceInfo;
 
 /**
  * @hide
@@ -122,4 +123,8 @@
     // Enables or disables test mode
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     boolean setTestMode(in boolean enable);
+
+    // Finds all endpoints that havea specific ID
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    List<HubEndpointInfo> findEndpoints(long endpointId);
 }
diff --git a/core/java/android/net/vcn/VcnFrameworkInitializer.java b/core/java/android/net/ConnectivityFrameworkInitializerBaklava.java
similarity index 86%
rename from core/java/android/net/vcn/VcnFrameworkInitializer.java
rename to core/java/android/net/ConnectivityFrameworkInitializerBaklava.java
index 8cb213b..1f0fa92 100644
--- a/core/java/android/net/vcn/VcnFrameworkInitializer.java
+++ b/core/java/android/net/ConnectivityFrameworkInitializerBaklava.java
@@ -14,15 +14,21 @@
  * limitations under the License.
  */
 
-package android.net.vcn;
+package android.net;
 
+import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API;
+
+import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.SystemServiceRegistry;
 import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.net.vcn.IVcnManagementService;
+import android.net.vcn.VcnManager;
 import android.os.Build;
 import android.os.SystemProperties;
 
@@ -31,8 +37,9 @@
  *
  * @hide
  */
-// TODO: Expose it as @SystemApi(client = MODULE_LIBRARIES)
-public final class VcnFrameworkInitializer {
+@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ConnectivityFrameworkInitializerBaklava {
     /**
      * Starting with {@link VANILLA_ICE_CREAM}, Telephony feature flags (e.g. {@link
      * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}) are being checked before returning managers
@@ -55,7 +62,7 @@
      */
     private static final int VENDOR_API_FOR_ANDROID_V = 202404;
 
-    private VcnFrameworkInitializer() {}
+    private ConnectivityFrameworkInitializerBaklava() {}
 
     // Suppressing AndroidFrameworkCompatChange because we're querying vendor
     // partition SDK level, not application's target SDK version (which BTW we
@@ -86,7 +93,10 @@
      *
      * @throws IllegalStateException if this is called anywhere besides {@link
      *     SystemServiceRegistry}.
+     * @hide
      */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static void registerServiceWrappers() {
         SystemServiceRegistry.registerContextAwareService(
                 VcnManager.VCN_MANAGEMENT_SERVICE_STRING,
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index af93c96..3219ce8 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -16,6 +16,7 @@
 package android.net.vcn;
 
 import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE;
+import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API;
 import static android.net.vcn.Flags.FLAG_SAFE_MODE_CONFIG;
 import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED;
 
@@ -82,7 +83,15 @@
  * </ul>
  */
 public final class VcnGatewayConnectionConfig {
-    /** @hide */
+    /**
+     * Minimum NAT timeout not set.
+     *
+     * <p>When the timeout is not set, the device will automatically choose a keepalive interval and
+     * may reduce the keepalive frequency for power-optimization.
+     */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    // This constant does not represent a minimum value. It indicates the value is not configured.
+    @SuppressLint("MinMaxConstant")
     public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1;
 
     /** @hide */
@@ -773,7 +782,7 @@
          *
          * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN
          *     Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN
-         *     Gateway.
+         *     Gateway; or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET} to clear this value.
          * @return this {@link Builder} instance, for chaining
          */
         @NonNull
@@ -781,8 +790,10 @@
                 @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS)
                         int minUdpPort4500NatTimeoutSeconds) {
             Preconditions.checkArgument(
-                    minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
-                    "Timeout must be at least 120s");
+                    minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET
+                            || minUdpPort4500NatTimeoutSeconds
+                                    >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
+                    "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET");
 
             mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds;
             return this;
diff --git a/core/java/android/net/vcn/VcnTransportInfo.java b/core/java/android/net/vcn/VcnTransportInfo.java
index 1fc91ee..3638429 100644
--- a/core/java/android/net/vcn/VcnTransportInfo.java
+++ b/core/java/android/net/vcn/VcnTransportInfo.java
@@ -17,13 +17,16 @@
 package android.net.vcn;
 
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API;
 import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS;
 import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.net.NetworkCapabilities;
 import android.net.TransportInfo;
 import android.net.wifi.WifiInfo;
@@ -52,23 +55,29 @@
  * @hide
  */
 // TODO: Do not store WifiInfo and subscription ID in VcnTransportInfo anymore
-public class VcnTransportInfo implements TransportInfo, Parcelable {
+@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class VcnTransportInfo implements TransportInfo, Parcelable {
     @Nullable private final WifiInfo mWifiInfo;
     private final int mSubId;
     private final int mMinUdpPort4500NatTimeoutSeconds;
 
+    /** @hide */
     public VcnTransportInfo(@NonNull WifiInfo wifiInfo) {
         this(wifiInfo, INVALID_SUBSCRIPTION_ID, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET);
     }
 
+    /** @hide */
     public VcnTransportInfo(@NonNull WifiInfo wifiInfo, int minUdpPort4500NatTimeoutSeconds) {
         this(wifiInfo, INVALID_SUBSCRIPTION_ID, minUdpPort4500NatTimeoutSeconds);
     }
 
+    /** @hide */
     public VcnTransportInfo(int subId) {
         this(null /* wifiInfo */, subId, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET);
     }
 
+    /** @hide */
     public VcnTransportInfo(int subId, int minUdpPort4500NatTimeoutSeconds) {
         this(null /* wifiInfo */, subId, minUdpPort4500NatTimeoutSeconds);
     }
@@ -86,6 +95,7 @@
      * <p>If the underlying Network for the associated VCN is Cellular, returns null.
      *
      * @return the WifiInfo if there is an underlying WiFi connection, else null.
+     * @hide
      */
     @Nullable
     public WifiInfo getWifiInfo() {
@@ -100,17 +110,27 @@
      *
      * @return the Subscription ID if a cellular underlying Network is present, else {@link
      *     android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+     * @hide
      */
     public int getSubId() {
         return mSubId;
     }
 
     /**
-     * Get the VCN provided UDP port 4500 NAT timeout
+     * Get the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping.
      *
-     * @return the UDP 4500 NAT timeout, or
+     * <p>To ensure uninterrupted connectivity, the device must send keepalive packets before the
+     * timeout. Failure to do so may result in the mapping being cleared and connection termination.
+     * This value is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs,
+     * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data.
+     *
+     * @return the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping, or
      *     VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET if not set.
+     * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int)
+     * @hide
      */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public int getMinUdpPort4500NatTimeoutSeconds() {
         return mMinUdpPort4500NatTimeoutSeconds;
     }
@@ -129,12 +149,21 @@
                 && mMinUdpPort4500NatTimeoutSeconds == that.mMinUdpPort4500NatTimeoutSeconds;
     }
 
-    /** {@inheritDoc} */
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @Override
     public int describeContents() {
         return 0;
     }
 
+    /** @hide */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @Override
     @NonNull
     public TransportInfo makeCopy(long redactions) {
@@ -149,6 +178,9 @@
                 mMinUdpPort4500NatTimeoutSeconds);
     }
 
+    /** @hide */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @Override
     public long getApplicableRedactions() {
         long redactions = REDACT_FOR_NETWORK_SETTINGS;
@@ -161,7 +193,13 @@
         return redactions;
     }
 
-    /** {@inheritDoc} */
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeInt(mSubId);
@@ -174,7 +212,13 @@
         return "VcnTransportInfo { mWifiInfo = " + mWifiInfo + ", mSubId = " + mSubId + " }";
     }
 
-    /** Implement the Parcelable interface */
+    /**
+     * Implement the Parcelable interface
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final @NonNull Creator<VcnTransportInfo> CREATOR =
             new Creator<VcnTransportInfo>() {
                 public VcnTransportInfo createFromParcel(Parcel in) {
@@ -201,37 +245,63 @@
                 }
             };
 
-    /** This class can be used to construct a {@link VcnTransportInfo}. */
+    /**
+     * This class can be used to construct a {@link VcnTransportInfo}.
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final class Builder {
         private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
 
-        /** Construct Builder */
+        /**
+         * Construct Builder
+         *
+         * @hide
+         */
+        @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         public Builder() {}
 
         /**
-         * Sets the maximum supported IKEv2/IPsec NATT keepalive timeout.
+         * Set the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping.
          *
          * <p>This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs,
          * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data.
          *
-         * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN
-         *     Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN
-         *     Gateway.
+         * @param minUdpPort4500NatTimeoutSeconds the minimum duration that the VCN Gateway
+         *     guarantees to preserve a NAT mapping, or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET}
+         *     to clear this value. To ensure uninterrupted connectivity, the device must send
+         *     keepalive packets within this interval. Failure to do so may result in the mapping
+         *     being cleared and connection termination.
          * @return this {@link Builder} instance, for chaining
+         * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int)
+         * @hide
          */
+        @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @NonNull
         public Builder setMinUdpPort4500NatTimeoutSeconds(
                 @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS)
                         int minUdpPort4500NatTimeoutSeconds) {
             Preconditions.checkArgument(
-                    minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
-                    "Timeout must be at least 120s");
+                    minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET
+                            || minUdpPort4500NatTimeoutSeconds
+                                    >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
+                    "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET");
 
             mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds;
             return Builder.this;
         }
 
-        /** Build a VcnTransportInfo instance */
+        /**
+         * Build a VcnTransportInfo instance
+         *
+         * @hide
+         */
+        @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API)
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @NonNull
         public VcnTransportInfo build() {
             return new VcnTransportInfo(
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index 99e7d166..05bd10b 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -18,10 +18,12 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.util.ArrayMap;
 import android.util.Size;
@@ -72,16 +74,18 @@
     /**
      * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain
      * Binder object(s).
-     *
      * @hide
      */
+    @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int STATUS_BINDERS_NOT_PRESENT = 0;
 
     /**
      * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel.
-     *
      * @hide
      */
+    @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int STATUS_BINDERS_PRESENT = 1;
 
     /**
@@ -94,9 +98,10 @@
      * object to the Bundle but it is not possible to assert this fact unless the Bundle is written
      * to a Parcel.
      * </p>
-     *
      * @hide
      */
+    @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int STATUS_BINDERS_UNKNOWN = 2;
 
     /** @hide */
@@ -417,6 +422,8 @@
      * Returns a status indicating whether the bundle contains any parcelled Binder objects.
      * @hide
      */
+    @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public @HasBinderStatus int hasBinders() {
         if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) {
             if ((mFlags & FLAG_HAS_BINDERS) != 0) {
diff --git a/core/java/android/os/CpuHeadroomParams.java b/core/java/android/os/CpuHeadroomParams.java
new file mode 100644
index 0000000..f0d4f7d
--- /dev/null
+++ b/core/java/android/os/CpuHeadroomParams.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.os.health.SystemHealthManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Headroom request params used by {@link SystemHealthManager#getCpuHeadroom(CpuHeadroomParams)}.
+ */
+@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
+public final class CpuHeadroomParams {
+    final CpuHeadroomParamsInternal mInternal;
+
+    public CpuHeadroomParams() {
+        mInternal = new CpuHeadroomParamsInternal();
+    }
+
+    /** @hide */
+    @IntDef(flag = false, prefix = {"CPU_HEADROOM_CALCULATION_TYPE_"}, value = {
+            CPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
+            CPU_HEADROOM_CALCULATION_TYPE_AVERAGE, // 1
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CpuHeadroomCalculationType {
+    }
+
+    /**
+     * Calculates the headroom based on minimum value over a device-defined window.
+     */
+    public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
+
+    /**
+     * Calculates the headroom based on average value over a device-defined window.
+     */
+    public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
+
+    /**
+     * Sets the headroom calculation type.
+     * <p>
+     *
+     * @throws IllegalArgumentException if the type is invalid.
+     */
+    public void setCalculationType(@CpuHeadroomCalculationType int calculationType) {
+        switch (calculationType) {
+            case CPU_HEADROOM_CALCULATION_TYPE_MIN:
+            case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
+                mInternal.calculationType = (byte) calculationType;
+                return;
+        }
+        throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+    }
+
+    /**
+     * Gets the headroom calculation type.
+     * Default to {@link #CPU_HEADROOM_CALCULATION_TYPE_MIN} if not set.
+     */
+    public @CpuHeadroomCalculationType int getCalculationType() {
+        @CpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
+            case CPU_HEADROOM_CALCULATION_TYPE_MIN, CPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
+                    mInternal.calculationType;
+            default -> CPU_HEADROOM_CALCULATION_TYPE_MIN;
+        };
+        return validatedType;
+    }
+
+    /**
+     * @hide
+     */
+    public CpuHeadroomParamsInternal getInternal() {
+        return mInternal;
+    }
+}
diff --git a/core/java/android/os/CpuHeadroomParamsInternal.aidl b/core/java/android/os/CpuHeadroomParamsInternal.aidl
new file mode 100644
index 0000000..6cc4699
--- /dev/null
+++ b/core/java/android/os/CpuHeadroomParamsInternal.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.hardware.power.CpuHeadroomParams;
+
+/**
+ * Changes should be synced with match function of HintManagerService#CpuHeadroomCacheItem.
+ * {@hide}
+ */
+@JavaDerive(equals = true, toString = true)
+parcelable CpuHeadroomParamsInternal {
+    boolean usesDeviceHeadroom = false;
+    CpuHeadroomParams.CalculationType calculationType = CpuHeadroomParams.CalculationType.MIN;
+    CpuHeadroomParams.SelectionType selectionType = CpuHeadroomParams.SelectionType.ALL;
+}
+
diff --git a/core/java/android/os/GpuHeadroomParams.java b/core/java/android/os/GpuHeadroomParams.java
new file mode 100644
index 0000000..efb2a28
--- /dev/null
+++ b/core/java/android/os/GpuHeadroomParams.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.os.health.SystemHealthManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Headroom request params used by {@link SystemHealthManager#getGpuHeadroom(GpuHeadroomParams)}.
+ */
+@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
+public final class GpuHeadroomParams {
+    final GpuHeadroomParamsInternal mInternal;
+
+    public GpuHeadroomParams() {
+        mInternal = new GpuHeadroomParamsInternal();
+    }
+
+    /** @hide */
+    @IntDef(flag = false, prefix = {"GPU_HEADROOM_CALCULATION_TYPE_"}, value = {
+            GPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
+            GPU_HEADROOM_CALCULATION_TYPE_AVERAGE, // 1
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GpuHeadroomCalculationType {
+    }
+
+    /**
+     * Calculates the headroom based on minimum value over a device-defined window.
+     */
+    public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
+
+    /**
+     * Calculates the headroom based on average value over a device-defined window.
+     */
+    public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
+
+    /**
+     * Sets the headroom calculation type.
+     * <p>
+     *
+     * @throws IllegalArgumentException if the type is invalid.
+     */
+    public void setCalculationType(@GpuHeadroomCalculationType int calculationType) {
+        switch (calculationType) {
+            case GPU_HEADROOM_CALCULATION_TYPE_MIN:
+            case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
+                mInternal.calculationType = (byte) calculationType;
+                return;
+        }
+        throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+    }
+
+    /**
+     * Gets the headroom calculation type.
+     * Default to {@link #GPU_HEADROOM_CALCULATION_TYPE_MIN} if not set.
+     */
+    public @GpuHeadroomCalculationType int getCalculationType() {
+        @GpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
+            case GPU_HEADROOM_CALCULATION_TYPE_MIN, GPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
+                    mInternal.calculationType;
+            default -> GPU_HEADROOM_CALCULATION_TYPE_MIN;
+        };
+        return validatedType;
+    }
+
+    /**
+     * @hide
+     */
+    public GpuHeadroomParamsInternal getInternal() {
+        return mInternal;
+    }
+}
diff --git a/core/java/android/os/GpuHeadroomParamsInternal.aidl b/core/java/android/os/GpuHeadroomParamsInternal.aidl
new file mode 100644
index 0000000..20309e7
--- /dev/null
+++ b/core/java/android/os/GpuHeadroomParamsInternal.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.hardware.power.GpuHeadroomParams;
+
+/**
+ * Changes should be synced with match function of HintManagerService#GpuHeadroomCacheItem.
+ * {@hide}
+ */
+@JavaDerive(equals = true, toString = true)
+parcelable GpuHeadroomParamsInternal {
+    GpuHeadroomParams.CalculationType calculationType = GpuHeadroomParams.CalculationType.MIN;
+}
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 73cdd56..3312055 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -17,6 +17,8 @@
 
 package android.os;
 
+import android.os.CpuHeadroomParamsInternal;
+import android.os.GpuHeadroomParamsInternal;
 import android.os.IHintSession;
 import android.hardware.power.ChannelConfig;
 import android.hardware.power.SessionConfig;
@@ -50,4 +52,8 @@
      */
     @nullable ChannelConfig getSessionChannel(in IBinder token);
     oneway void closeSessionChannel();
+    float[] getCpuHeadroom(in CpuHeadroomParamsInternal params);
+    long getCpuHeadroomMinIntervalMillis();
+    float getGpuHeadroom(in GpuHeadroomParamsInternal params);
+    long getGpuHeadroomMinIntervalMillis();
 }
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 590ddb4..24e1d66 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -78,6 +78,9 @@
 # PermissionEnforcer
 per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com
 
+# RemoteCallbackList
+per-file RemoteCallbackList.java = shayba@google.com
+
 # ART
 per-file ArtModuleServiceManager.java = file:platform/art:/OWNERS
 
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 9ab9228..5a53bc15 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5308,7 +5308,13 @@
             Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS,
             Manifest.permission.QUERY_USERS}, conditional = true)
+    @CachedProperty(api = "user_manager_user_data")
     public List<UserInfo> getProfiles(@UserIdInt int userId) {
+        if (android.multiuser.Flags.cacheProfilesReadOnly()) {
+            return UserManagerCache.getProfiles(
+                    (Integer userIdentifier) -> mService.getProfiles(userIdentifier, false),
+                    userId);
+        }
         try {
             return mService.getProfiles(userId, false /* enabledOnly */);
         } catch (RemoteException re) {
@@ -6484,6 +6490,19 @@
     }
 
     /**
+     * This method is used to invalidate caches, when UserManagerService.mUsers
+     * {@link UserManagerService.UserData} is modified, including changes to {@link UserInfo}.
+     * In practice we determine modification by when that data is persisted, or scheduled to be
+     * presisted, to xml.
+     * @hide
+     */
+    public static final void invalidateCacheOnUserDataChanged() {
+        if (android.multiuser.Flags.cacheProfilesReadOnly()) {
+            UserManagerCache.invalidateProfiles();
+        }
+    }
+
+    /**
      * Returns a serial number on this device for a given userId. User handles can be recycled
      * when deleting and creating users, but serial numbers are not reused until the device is
      * wiped.
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index d9db28e..118167d 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -66,6 +66,14 @@
 }
 
 flag {
+    name: "adpf_use_load_hints"
+    namespace: "game"
+    description: "Guards use of the ADPF public load hints behind a readonly flag"
+    is_fixed_read_only: true
+    bug: "367803904"
+}
+
+flag {
     name: "allow_consentless_bugreport_delegated_consent"
     namespace: "crumpet"
     description: "Allow privileged apps to call bugreport generation without enforcing user consent and delegate it to the calling app instead"
@@ -148,6 +156,13 @@
 }
 
 flag {
+    name: "cpu_gpu_headrooms"
+    namespace: "game"
+    description: "Feature flag for adding CPU/GPU headroom API"
+    bug: "346604998"
+}
+
+flag {
     name: "disallow_cellular_null_ciphers_restriction"
     namespace: "cellular_security"
     description: "Guards a new UserManager user restriction that admins can use to require cellular encryption on their managed devices."
@@ -253,6 +268,15 @@
 
 flag {
      namespace: "system_performance"
+     name: "enable_has_binders"
+     is_exported: true
+     description: "Add hasBinders to Public API under a flag."
+     is_fixed_read_only: true
+     bug: "330345513"
+}
+
+flag {
+     namespace: "system_performance"
      name: "perfetto_sdk_tracing"
      description: "Tracing using Perfetto SDK."
      bug: "303199244"
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index deabfed..4db9bc3 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -17,6 +17,7 @@
 package android.os.health;
 
 import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
@@ -25,6 +26,11 @@
 import android.os.BatteryStats;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.CpuHeadroomParams;
+import android.os.CpuHeadroomParamsInternal;
+import android.os.GpuHeadroomParams;
+import android.os.GpuHeadroomParamsInternal;
+import android.os.IHintManager;
 import android.os.IPowerStatsService;
 import android.os.OutcomeReceiver;
 import android.os.PowerMonitor;
@@ -68,6 +74,8 @@
     private final IBatteryStats mBatteryStats;
     @Nullable
     private final IPowerStatsService mPowerStats;
+    @Nullable
+    private final IHintManager mHintManager;
     private List<PowerMonitor> mPowerMonitorsInfo;
     private final Object mPowerMonitorsLock = new Object();
     private static final long TAKE_UID_SNAPSHOT_TIMEOUT_MILLIS = 10_000;
@@ -88,14 +96,111 @@
     public SystemHealthManager() {
         this(IBatteryStats.Stub.asInterface(ServiceManager.getService(BatteryStats.SERVICE_NAME)),
                 IPowerStatsService.Stub.asInterface(
-                        ServiceManager.getService(Context.POWER_STATS_SERVICE)));
+                        ServiceManager.getService(Context.POWER_STATS_SERVICE)),
+                IHintManager.Stub.asInterface(
+                        ServiceManager.getService(Context.PERFORMANCE_HINT_SERVICE)));
     }
 
     /** {@hide} */
     public SystemHealthManager(@NonNull IBatteryStats batteryStats,
-            @Nullable IPowerStatsService powerStats) {
+            @Nullable IPowerStatsService powerStats, @Nullable IHintManager hintManager) {
         mBatteryStats = batteryStats;
         mPowerStats = powerStats;
+        mHintManager = hintManager;
+    }
+
+    /**
+     * Provides an estimate of global available CPU headroom of the calling thread.
+     * <p>
+     *
+     * @param  params params to customize the CPU headroom calculation, null to use default params.
+     * @return a single value a {@code Float.NaN} if it's temporarily unavailable.
+     *         A valid value is ranged from [0, 100], where 0 indicates no more CPU resources can be
+     *         granted.
+     * @throws UnsupportedOperationException if the API is unsupported or the request params can't
+     *         be served.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    public @FloatRange(from = 0f, to = 100f) float getCpuHeadroom(
+            @Nullable CpuHeadroomParams params) {
+        if (mHintManager == null) {
+            throw new UnsupportedOperationException();
+        }
+        try {
+            return mHintManager.getCpuHeadroom(
+                    params != null ? params.getInternal() : new CpuHeadroomParamsInternal())[0];
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+
+
+    /**
+     * Provides an estimate of global available GPU headroom of the device.
+     * <p>
+     *
+     * @param  params params to customize the GPU headroom calculation, null to use default params.
+     * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
+     *         A valid value is ranged from [0, 100], where 0 indicates no more GPU resources can be
+     *         granted.
+     * @throws UnsupportedOperationException if the API is unsupported or the request params can't
+     *         be served.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    public @FloatRange(from = 0f, to = 100f) float getGpuHeadroom(
+            @Nullable GpuHeadroomParams params) {
+        if (mHintManager == null) {
+            throw new UnsupportedOperationException();
+        }
+        try {
+            return mHintManager.getGpuHeadroom(
+                    params != null ? params.getInternal() : new GpuHeadroomParamsInternal());
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
+     * milliseconds.
+     * <p>
+     * The {@link #getCpuHeadroom(CpuHeadroomParams)} API may return cached result if called more
+     * frequent than the interval.
+     *
+     * @throws UnsupportedOperationException if the API is unsupported.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    public long getCpuHeadroomMinIntervalMillis() {
+        if (mHintManager == null) {
+            throw new UnsupportedOperationException();
+        }
+        try {
+            return mHintManager.getCpuHeadroomMinIntervalMillis();
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
+     * milliseconds.
+     * <p>
+     * The {@link #getGpuHeadroom(GpuHeadroomParams)} API may return cached result if called more
+     * frequent than the interval.
+     *
+     * @throws UnsupportedOperationException if the API is unsupported.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    public long getGpuHeadroomMinIntervalMillis() {
+        if (mHintManager == null) {
+            throw new UnsupportedOperationException();
+        }
+        try {
+            return mHintManager.getGpuHeadroomMinIntervalMillis();
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -261,7 +366,7 @@
                         mPowerMonitorsInfo = result;
                     }
                     if (executor != null) {
-                        executor.execute(()-> onResult.accept(result));
+                        executor.execute(() -> onResult.accept(result));
                     } else {
                         onResult.accept(result);
                     }
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index ce90121..09004b3 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -115,6 +115,14 @@
 }
 
 flag {
+    name: "protect_device_config_flags"
+    namespace: "psap_ai"
+    description: "Feature flag to limit adb shell to allowlisted flags"
+    bug: "364083026"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "keystore_grant_api"
     namespace: "hardware_backed_security"
     description: "Feature flag for exposing KeyStore grant APIs"
diff --git a/core/java/android/security/forensic/ForensicManager.java b/core/java/android/security/forensic/ForensicManager.java
new file mode 100644
index 0000000..9126182
--- /dev/null
+++ b/core/java/android/security/forensic/ForensicManager.java
@@ -0,0 +1,276 @@
+/*
+ * 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.forensic;
+
+import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
+import static android.Manifest.permission.READ_FORENSIC_STATE;
+
+import android.annotation.CallbackExecutor;
+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 android.security.Flags;
+import android.util.Log;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * ForensicManager manages the forensic logging on Android devices.
+ * Upon user consent, forensic logging collects various device events for
+ * off-device investigation of potential device compromise.
+ * <p>
+ * Forensic 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}.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_AFL_API)
+@SystemService(Context.FORENSIC_SERVICE)
+public class ForensicManager {
+    private static final String TAG = "ForensicManager";
+
+    /** @hide */
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_UNKNOWN,
+            STATE_DISABLED,
+            STATE_ENABLED
+    })
+    public @interface ForensicState {}
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_UNKNOWN,
+            ERROR_PERMISSION_DENIED,
+            ERROR_TRANSPORT_UNAVAILABLE,
+            ERROR_DATA_SOURCE_UNAVAILABLE
+    })
+    public @interface ForensicError {}
+
+    /**
+     * Indicates an unknown state
+     */
+    public static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
+
+    /**
+     * Indicates an state that the forensic is turned off.
+     */
+    public static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
+
+    /**
+     * Indicates an state that the forensic is turned on.
+     */
+    public static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
+
+    /**
+     * Indicates an unknown error
+     */
+    public static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
+
+    /**
+     * Indicates an error due to insufficient access rights.
+     */
+    public static final int ERROR_PERMISSION_DENIED =
+            IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+
+    /**
+     * Indicates an error due to unavailability of the forensic event transport.
+     */
+    public static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IForensicServiceCommandCallback.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;
+
+
+    private final IForensicService mService;
+
+    private final ConcurrentHashMap<Consumer<Integer>, IForensicServiceStateCallback>
+            mStateCallbacks = new ConcurrentHashMap<>();
+
+    /**
+     * Constructor
+     *
+     * @param service A valid instance of IForensicService.
+     * @hide
+     */
+    public ForensicManager(IForensicService service) {
+        mService = service;
+    }
+
+    /**
+     * Add a callback to monitor the state of the ForensicService.
+     *
+     * @param executor The executor through which the callback should be invoked.
+     * @param callback The callback for state change.
+     *                 Once the callback is registered, the callback will be called
+     *                 to reflect the init state.
+     *                 The callback can be registered only once.
+     */
+    @RequiresPermission(READ_FORENSIC_STATE)
+    public void addStateCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull @ForensicState Consumer<Integer> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        if (mStateCallbacks.get(callback) != null) {
+            Log.d(TAG, "addStateCallback callback already present");
+            return;
+        }
+
+        final IForensicServiceStateCallback wrappedCallback =
+                new IForensicServiceStateCallback.Stub() {
+                    @Override
+                    public void onStateChange(int state) {
+                        executor.execute(() -> callback.accept(state));
+                    }
+                };
+        try {
+            mService.addStateCallback(wrappedCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        mStateCallbacks.put(callback, wrappedCallback);
+    }
+
+    /**
+     * Remove a callback to monitor the state of the ForensicService.
+     *
+     * @param callback The callback to remove.
+     */
+    @RequiresPermission(READ_FORENSIC_STATE)
+    public void removeStateCallback(@NonNull Consumer<@ForensicState Integer> callback) {
+        Objects.requireNonNull(callback);
+        if (!mStateCallbacks.containsKey(callback)) {
+            Log.d(TAG, "removeStateCallback callback not present");
+            return;
+        }
+
+        IForensicServiceStateCallback wrappedCallback = mStateCallbacks.get(callback);
+
+        try {
+            mService.removeStateCallback(wrappedCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        mStateCallbacks.remove(callback);
+    }
+
+    /**
+     * Enable forensic logging.
+     * If successful, ForensicService 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}.
+     *
+     * @param executor The executor through which the callback should be invoked.
+     * @param callback The callback for the command result.
+     */
+    @RequiresPermission(MANAGE_FORENSIC_STATE)
+    public void enable(@NonNull @CallbackExecutor Executor executor,
+            @NonNull CommandCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        try {
+            mService.enable(new IForensicServiceCommandCallback.Stub() {
+                @Override
+                public void onSuccess() {
+                    executor.execute(callback::onSuccess);
+                }
+
+                @Override
+                public void onFailure(int error) {
+                    executor.execute(() -> callback.onFailure(error));
+                }
+            });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Disable forensic logging.
+     * If successful, ForensicService 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
+     * 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)
+    public void disable(@NonNull @CallbackExecutor Executor executor,
+            @NonNull CommandCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        try {
+            mService.disable(new IForensicServiceCommandCallback.Stub() {
+                @Override
+                public void onSuccess() {
+                    executor.execute(callback::onSuccess);
+                }
+
+                @Override
+                public void onFailure(int error) {
+                    executor.execute(() -> callback.onFailure(error));
+                }
+            });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callback used in {@link #enable} and {@link #disable} to indicate the result of the command.
+     */
+    public interface CommandCallback {
+        /**
+         * Called when command succeeds.
+         */
+        void onSuccess();
+
+        /**
+         * Called when command fails.
+         * @param error The error number.
+         */
+        void onFailure(@ForensicError int error);
+    }
+}
diff --git a/core/java/android/security/forensic/IBackupTransport.aidl b/core/java/android/security/forensic/IForensicEventTransport.aidl
similarity index 96%
rename from core/java/android/security/forensic/IBackupTransport.aidl
rename to core/java/android/security/forensic/IForensicEventTransport.aidl
index c2cbc83..80e78eb 100644
--- a/core/java/android/security/forensic/IBackupTransport.aidl
+++ b/core/java/android/security/forensic/IForensicEventTransport.aidl
@@ -20,7 +20,7 @@
 import com.android.internal.infra.AndroidFuture;
 
 /** {@hide} */
-oneway interface IBackupTransport {
+oneway interface IForensicEventTransport {
     /**
      * Initialize the server side.
      */
diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl
index a944b18..8039b26 100644
--- a/core/java/android/security/forensic/IForensicService.aidl
+++ b/core/java/android/security/forensic/IForensicService.aidl
@@ -24,9 +24,12 @@
  * @hide
  */
 interface IForensicService {
-    void monitorState(IForensicServiceStateCallback callback);
-    void makeVisible(IForensicServiceCommandCallback callback);
-    void makeInvisible(IForensicServiceCommandCallback callback);
+    @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/IForensicServiceCommandCallback.aidl b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
index 7fa0c7f..6d1456e 100644
--- a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
+++ b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
@@ -25,8 +25,8 @@
          UNKNOWN = 0,
          PERMISSION_DENIED = 1,
          INVALID_STATE_TRANSITION = 2,
-         BACKUP_TRANSPORT_UNAVAILABLE = 3,
-         DATA_SOURCE_UNAVAILABLE = 3,
+         TRANSPORT_UNAVAILABLE = 3,
+         DATA_SOURCE_UNAVAILABLE = 4,
      }
     void onSuccess();
     void onFailure(ErrorCode error);
diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
index 0cda350..1b68c7b 100644
--- a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
+++ b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
@@ -23,9 +23,8 @@
     @Backing(type="int")
     enum State{
         UNKNOWN = 0,
-        INVISIBLE = 1,
-        VISIBLE = 2,
-        ENABLED = 3,
+        DISABLED = 1,
+        ENABLED = 2,
     }
     void onStateChange(State state);
  }
diff --git a/core/java/android/service/quickaccesswallet/flags.aconfig b/core/java/android/service/quickaccesswallet/flags.aconfig
index 07311d5..75a9309 100644
--- a/core/java/android/service/quickaccesswallet/flags.aconfig
+++ b/core/java/android/service/quickaccesswallet/flags.aconfig
@@ -3,7 +3,7 @@
 
 flag {
     name: "launch_wallet_option_on_power_double_tap"
-    namespace: "wallet_integrations"
+    namespace: "wallet_integration"
     description: "Option to launch the Wallet app on double-tap of the power button"
     bug: "378469025"
 }
\ No newline at end of file
diff --git a/core/java/android/telephony/SubscriptionPlan.java b/core/java/android/telephony/SubscriptionPlan.java
index 7b48a16..4c59a85 100644
--- a/core/java/android/telephony/SubscriptionPlan.java
+++ b/core/java/android/telephony/SubscriptionPlan.java
@@ -18,6 +18,7 @@
 
 import android.annotation.BytesLong;
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -28,6 +29,7 @@
 import android.util.Range;
 import android.util.RecurrenceRule;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -83,6 +85,33 @@
     /** Value indicating a timestamp is unknown. */
     public static final long TIME_UNKNOWN = -1;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "SUBSCRIPTION_STATUS_" }, value = {
+            SUBSCRIPTION_STATUS_UNKNOWN,
+            SUBSCRIPTION_STATUS_ACTIVE,
+            SUBSCRIPTION_STATUS_INACTIVE,
+            SUBSCRIPTION_STATUS_TRIAL,
+            SUBSCRIPTION_STATUS_SUSPENDED
+    })
+    public @interface SubscriptionStatus {}
+
+    /** Subscription status is unknown. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public static final int SUBSCRIPTION_STATUS_UNKNOWN = 0;
+    /** Subscription is active. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public static final int SUBSCRIPTION_STATUS_ACTIVE = 1;
+    /** Subscription is inactive. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public static final int SUBSCRIPTION_STATUS_INACTIVE = 2;
+    /** Subscription is in a trial period. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public static final int SUBSCRIPTION_STATUS_TRIAL = 3;
+    /** Subscription is suspended. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public static final int SUBSCRIPTION_STATUS_SUSPENDED = 4;
+
     private final RecurrenceRule cycleRule;
     private CharSequence title;
     private CharSequence summary;
@@ -91,6 +120,7 @@
     private long dataUsageBytes = BYTES_UNKNOWN;
     private long dataUsageTime = TIME_UNKNOWN;
     private @NetworkType int[] networkTypes;
+    private int mSubscriptionStatus = SUBSCRIPTION_STATUS_UNKNOWN;
 
     private SubscriptionPlan(RecurrenceRule cycleRule) {
         this.cycleRule = Preconditions.checkNotNull(cycleRule);
@@ -107,6 +137,7 @@
         dataUsageBytes = source.readLong();
         dataUsageTime = source.readLong();
         networkTypes = source.createIntArray();
+        mSubscriptionStatus = source.readInt();
     }
 
     @Override
@@ -124,6 +155,7 @@
         dest.writeLong(dataUsageBytes);
         dest.writeLong(dataUsageTime);
         dest.writeIntArray(networkTypes);
+        dest.writeInt(mSubscriptionStatus);
     }
 
     @Override
@@ -137,13 +169,14 @@
                 .append(" dataUsageBytes=").append(dataUsageBytes)
                 .append(" dataUsageTime=").append(dataUsageTime)
                 .append(" networkTypes=").append(Arrays.toString(networkTypes))
+                .append(" subscriptionStatus=").append(mSubscriptionStatus)
                 .append("}").toString();
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(cycleRule, title, summary, dataLimitBytes, dataLimitBehavior,
-                dataUsageBytes, dataUsageTime, Arrays.hashCode(networkTypes));
+                dataUsageBytes, dataUsageTime, Arrays.hashCode(networkTypes), mSubscriptionStatus);
     }
 
     @Override
@@ -157,7 +190,8 @@
                     && dataLimitBehavior == other.dataLimitBehavior
                     && dataUsageBytes == other.dataUsageBytes
                     && dataUsageTime == other.dataUsageTime
-                    && Arrays.equals(networkTypes, other.networkTypes);
+                    && Arrays.equals(networkTypes, other.networkTypes)
+                    && mSubscriptionStatus == other.mSubscriptionStatus;
         }
         return false;
     }
@@ -179,6 +213,13 @@
         return cycleRule;
     }
 
+    /** Return the end date of this plan, or null if no end date exists. */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public @Nullable ZonedDateTime getPlanEndDate() {
+        // ZonedDateTime is immutable, so no need to create a defensive copy.
+        return cycleRule.end;
+    }
+
     /** Return the short title of this plan. */
     public @Nullable CharSequence getTitle() {
         return title;
@@ -238,6 +279,16 @@
     }
 
     /**
+     * Returns the status of the subscription plan.
+     *
+     * @return The subscription status, or {@link #SUBSCRIPTION_STATUS_UNKNOWN} if not available.
+     */
+    @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+    public @SubscriptionStatus int getSubscriptionStatus() {
+        return mSubscriptionStatus;
+    }
+
+    /**
      * Builder for a {@link SubscriptionPlan}.
      */
     public static class Builder {
@@ -382,5 +433,21 @@
                     TelephonyManager.getAllNetworkTypes().length);
             return this;
         }
+
+        /**
+         * Set the subscription status.
+         *
+         * @param subscriptionStatus the current subscription status
+         */
+        @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE)
+        public @NonNull Builder setSubscriptionStatus(@SubscriptionStatus int subscriptionStatus) {
+            if (subscriptionStatus < SUBSCRIPTION_STATUS_UNKNOWN
+                    || subscriptionStatus > SUBSCRIPTION_STATUS_SUSPENDED) {
+                throw new IllegalArgumentException(
+                        "Subscription status must be defined with a valid value");
+            }
+            plan.mSubscriptionStatus = subscriptionStatus;
+            return this;
+        }
     }
 }
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
deleted file mode 100644
index f69a333..0000000
--- a/core/java/android/text/TextFlags.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.text;
-
-import android.annotation.NonNull;
-import android.app.AppGlobals;
-
-/**
- * Flags in the "text" namespace.
- *
- * TODO(nona): Remove this class.
- * @hide
- */
-public final class TextFlags {
-
-    /**
-     * The name space of the "text" feature.
-     *
-     * This needs to move to DeviceConfig constant.
-     */
-    public static final String NAMESPACE = "text";
-
-    /**
-     * Whether we use the new design of context menu.
-     */
-    public static final String ENABLE_NEW_CONTEXT_MENU =
-            "TextEditing__enable_new_context_menu";
-
-    /**
-     * The key name used in app core settings for {@link #ENABLE_NEW_CONTEXT_MENU}.
-     */
-    public static final String KEY_ENABLE_NEW_CONTEXT_MENU = "text__enable_new_context_menu";
-
-    /**
-     * Default value for the flag {@link #ENABLE_NEW_CONTEXT_MENU}.
-     */
-    public static final boolean ENABLE_NEW_CONTEXT_MENU_DEFAULT = true;
-
-    /**
-     * List of text flags to be transferred to the application process.
-     */
-    public static final String[] TEXT_ACONFIGS_FLAGS = {
-    };
-
-    /**
-     * List of the default values of the text flags.
-     *
-     * The order must be the same to the TEXT_ACONFIG_FLAGS.
-     */
-    public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = {
-    };
-
-    /**
-     * Get a key for the feature flag.
-     */
-    public static String getKeyForFlag(@NonNull String flag) {
-        return "text__" + flag;
-    }
-
-    /**
-     * Return true if the feature flag is enabled.
-     */
-    public static boolean isFeatureEnabled(@NonNull String flag) {
-        return AppGlobals.getIntCoreSetting(
-                getKeyForFlag(flag), 0 /* aconfig is false by default */) != 0;
-    }
-}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 68efa79..d56768d 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -4566,14 +4566,31 @@
             return this;
         }
 
-        /** @hide */
+        /**
+         * Sets the Luts for the layer.
+         *
+         * <p> The function also allows to clear previously applied lut(s). To do this,
+         * set the displayluts to be either {@code nullptr} or
+         * an empty {@link android.hardware.DisplayLuts} instance.
+         *
+         * @param sc The SurfaceControl to update
+         *
+         * @param displayLuts The selected Lut(s)
+         *
+         * @return this
+         * @see DisplayLuts
+         */
+        @FlaggedApi(android.hardware.flags.Flags.FLAG_LUTS_API)
         public @NonNull Transaction setLuts(@NonNull SurfaceControl sc,
-                @NonNull DisplayLuts displayLuts) {
+                @Nullable DisplayLuts displayLuts) {
             checkPreconditions(sc);
-
-            nativeSetLuts(mNativeObject, sc.mNativeObject, displayLuts.getLutBuffers(),
-                    displayLuts.getOffsets(), displayLuts.getLutDimensions(),
-                    displayLuts.getLutSizes(), displayLuts.getLutSamplingKeys());
+            if (displayLuts != null && displayLuts.valid()) {
+                nativeSetLuts(mNativeObject, sc.mNativeObject, displayLuts.getLutBuffers(),
+                        displayLuts.getOffsets(), displayLuts.getLutDimensions(),
+                        displayLuts.getLutSizes(), displayLuts.getLutSamplingKeys());
+            } else {
+                nativeSetLuts(mNativeObject, sc.mNativeObject, null, null, null, null, null);
+            }
             return this;
         }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e50662a..19d3dc4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2516,6 +2516,11 @@
     public void notifyInsetsAnimationRunningStateChanged(boolean running) {
         if (sToolkitSetFrameRateReadOnlyFlagValue) {
             mInsetsAnimationRunning = running;
+            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                Trace.instant(Trace.TRACE_TAG_VIEW,
+                        TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
+                        Boolean.toString(running)));
+            }
         }
     }
 
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index a560339..afe195c 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -523,7 +523,6 @@
     @Nullable
     public LocaleList hintLocales = null;
 
-
     /**
      * List of acceptable MIME types for
      * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)}.
@@ -758,6 +757,30 @@
         return mIsStylusHandwritingEnabled;
     }
 
+    private boolean mWritingToolsEnabled = true;
+
+    /**
+     * Returns {@code true} when an {@code Editor} has writing tools enabled.
+     * {@code true} by default for all editors. Toolkits can optionally disable them where not
+     * relevant e.g. passwords, number input, etc.
+     * @see #setWritingToolsEnabled(boolean)
+     */
+    @FlaggedApi(Flags.FLAG_WRITING_TOOLS)
+    public boolean isWritingToolsEnabled() {
+        return mWritingToolsEnabled;
+    }
+
+    /**
+     * Set {@code false} if {@code Editor} opts-out of writing tools, that enable IMEs to replace
+     * text with generative AI text.
+     * @param enabled set {@code true} to enabled or {@code false to disable} support.
+     * @see #isWritingToolsEnabled()
+     */
+    @FlaggedApi(Flags.FLAG_WRITING_TOOLS)
+    public void setWritingToolsEnabled(boolean enabled) {
+        mWritingToolsEnabled = enabled;
+    }
+
     /**
      * If not {@code null}, this editor needs to talk to IMEs that run for the specified user, no
      * matter what user ID the calling process has.
@@ -1276,6 +1299,7 @@
                 + InputMethodDebug.handwritingGestureTypeFlagsToString(
                         mSupportedHandwritingGesturePreviewTypes));
         pw.println(prefix + "isStylusHandwritingEnabled=" + mIsStylusHandwritingEnabled);
+        pw.println(prefix + "writingToolsEnabled=" + mWritingToolsEnabled);
         pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes));
         if (targetInputMethodUser != null) {
             pw.println(prefix + "targetInputMethodUserId=" + targetInputMethodUser.getIdentifier());
@@ -1356,6 +1380,7 @@
         }
         dest.writeStringArray(contentMimeTypes);
         UserHandle.writeToParcel(targetInputMethodUser, dest);
+        dest.writeBoolean(mWritingToolsEnabled);
     }
 
     /**
@@ -1396,6 +1421,7 @@
                     res.hintLocales = hintLocales.isEmpty() ? null : hintLocales;
                     res.contentMimeTypes = source.readStringArray();
                     res.targetInputMethodUser = UserHandle.readFromParcel(source);
+                    res.mWritingToolsEnabled = source.readBoolean();
                     return res;
                 }
 
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d7750bd..cb70466 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -106,6 +106,7 @@
 import android.os.ParcelableParcel;
 import android.os.Process;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.BoringLayout;
@@ -9229,174 +9230,179 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        restartMarqueeIfNeeded();
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "TextView.onDraw");
+        try {
+            restartMarqueeIfNeeded();
 
-        // Draw the background for this view
-        super.onDraw(canvas);
+            // Draw the background for this view
+            super.onDraw(canvas);
 
-        final int compoundPaddingLeft = getCompoundPaddingLeft();
-        final int compoundPaddingTop = getCompoundPaddingTop();
-        final int compoundPaddingRight = getCompoundPaddingRight();
-        final int compoundPaddingBottom = getCompoundPaddingBottom();
-        final int scrollX = mScrollX;
-        final int scrollY = mScrollY;
-        final int right = mRight;
-        final int left = mLeft;
-        final int bottom = mBottom;
-        final int top = mTop;
-        final boolean isLayoutRtl = isLayoutRtl();
-        final int offset = getHorizontalOffsetForDrawables();
-        final int leftOffset = isLayoutRtl ? 0 : offset;
-        final int rightOffset = isLayoutRtl ? offset : 0;
+            final int compoundPaddingLeft = getCompoundPaddingLeft();
+            final int compoundPaddingTop = getCompoundPaddingTop();
+            final int compoundPaddingRight = getCompoundPaddingRight();
+            final int compoundPaddingBottom = getCompoundPaddingBottom();
+            final int scrollX = mScrollX;
+            final int scrollY = mScrollY;
+            final int right = mRight;
+            final int left = mLeft;
+            final int bottom = mBottom;
+            final int top = mTop;
+            final boolean isLayoutRtl = isLayoutRtl();
+            final int offset = getHorizontalOffsetForDrawables();
+            final int leftOffset = isLayoutRtl ? 0 : offset;
+            final int rightOffset = isLayoutRtl ? offset : 0;
 
-        final Drawables dr = mDrawables;
-        if (dr != null) {
-            /*
-             * Compound, not extended, because the icon is not clipped
-             * if the text height is smaller.
-             */
+            final Drawables dr = mDrawables;
+            if (dr != null) {
+                /*
+                 * Compound, not extended, because the icon is not clipped
+                 * if the text height is smaller.
+                 */
 
-            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
-            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
+                int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
+                int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
 
-            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
-            // Make sure to update invalidateDrawable() when changing this code.
-            if (dr.mShowing[Drawables.LEFT] != null) {
-                canvas.save();
-                canvas.translate(scrollX + mPaddingLeft + leftOffset,
-                        scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
-                dr.mShowing[Drawables.LEFT].draw(canvas);
-                canvas.restore();
+                // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+                // Make sure to update invalidateDrawable() when changing this code.
+                if (dr.mShowing[Drawables.LEFT] != null) {
+                    canvas.save();
+                    canvas.translate(scrollX + mPaddingLeft + leftOffset,
+                            scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
+                    dr.mShowing[Drawables.LEFT].draw(canvas);
+                    canvas.restore();
+                }
+
+                // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+                // Make sure to update invalidateDrawable() when changing this code.
+                if (dr.mShowing[Drawables.RIGHT] != null) {
+                    canvas.save();
+                    canvas.translate(scrollX + right - left - mPaddingRight
+                                    - dr.mDrawableSizeRight - rightOffset,
+                            scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
+                    dr.mShowing[Drawables.RIGHT].draw(canvas);
+                    canvas.restore();
+                }
+
+                // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+                // Make sure to update invalidateDrawable() when changing this code.
+                if (dr.mShowing[Drawables.TOP] != null) {
+                    canvas.save();
+                    canvas.translate(scrollX + compoundPaddingLeft
+                            + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
+                    dr.mShowing[Drawables.TOP].draw(canvas);
+                    canvas.restore();
+                }
+
+                // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+                // Make sure to update invalidateDrawable() when changing this code.
+                if (dr.mShowing[Drawables.BOTTOM] != null) {
+                    canvas.save();
+                    canvas.translate(scrollX + compoundPaddingLeft
+                                    + (hspace - dr.mDrawableWidthBottom) / 2,
+                            scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
+                    dr.mShowing[Drawables.BOTTOM].draw(canvas);
+                    canvas.restore();
+                }
             }
 
-            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
-            // Make sure to update invalidateDrawable() when changing this code.
-            if (dr.mShowing[Drawables.RIGHT] != null) {
-                canvas.save();
-                canvas.translate(scrollX + right - left - mPaddingRight
-                        - dr.mDrawableSizeRight - rightOffset,
-                         scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
-                dr.mShowing[Drawables.RIGHT].draw(canvas);
-                canvas.restore();
+            int color = mCurTextColor;
+
+            if (mLayout == null) {
+                assumeLayout();
             }
 
-            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
-            // Make sure to update invalidateDrawable() when changing this code.
-            if (dr.mShowing[Drawables.TOP] != null) {
-                canvas.save();
-                canvas.translate(scrollX + compoundPaddingLeft
-                        + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
-                dr.mShowing[Drawables.TOP].draw(canvas);
-                canvas.restore();
+            Layout layout = mLayout;
+
+            if (mHint != null && !mHideHint && mText.length() == 0) {
+                if (mHintTextColor != null) {
+                    color = mCurHintTextColor;
+                }
+
+                layout = mHintLayout;
             }
 
-            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
-            // Make sure to update invalidateDrawable() when changing this code.
-            if (dr.mShowing[Drawables.BOTTOM] != null) {
-                canvas.save();
-                canvas.translate(scrollX + compoundPaddingLeft
-                        + (hspace - dr.mDrawableWidthBottom) / 2,
-                         scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
-                dr.mShowing[Drawables.BOTTOM].draw(canvas);
-                canvas.restore();
-            }
-        }
+            mTextPaint.setColor(color);
+            mTextPaint.drawableState = getDrawableState();
 
-        int color = mCurTextColor;
+            canvas.save();
+            /*  Would be faster if we didn't have to do this. Can we chop the
+                (displayable) text so that we don't need to do this ever?
+            */
 
-        if (mLayout == null) {
-            assumeLayout();
-        }
+            int extendedPaddingTop = getExtendedPaddingTop();
+            int extendedPaddingBottom = getExtendedPaddingBottom();
 
-        Layout layout = mLayout;
+            final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+            final int maxScrollY = mLayout.getHeight() - vspace;
 
-        if (mHint != null && !mHideHint && mText.length() == 0) {
-            if (mHintTextColor != null) {
-                color = mCurHintTextColor;
+            float clipLeft = compoundPaddingLeft + scrollX;
+            float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
+            float clipRight = right - left - getCompoundPaddingRight() + scrollX;
+            float clipBottom = bottom - top + scrollY
+                    - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
+
+            if (mShadowRadius != 0) {
+                clipLeft += Math.min(0, mShadowDx - mShadowRadius);
+                clipRight += Math.max(0, mShadowDx + mShadowRadius);
+
+                clipTop += Math.min(0, mShadowDy - mShadowRadius);
+                clipBottom += Math.max(0, mShadowDy + mShadowRadius);
             }
 
-            layout = mHintLayout;
-        }
+            canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
 
-        mTextPaint.setColor(color);
-        mTextPaint.drawableState = getDrawableState();
+            int voffsetText = 0;
+            int voffsetCursor = 0;
 
-        canvas.save();
-        /*  Would be faster if we didn't have to do this. Can we chop the
-            (displayable) text so that we don't need to do this ever?
-        */
+            // translate in by our padding
+            /* shortcircuit calling getVerticaOffset() */
+            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+                voffsetText = getVerticalOffset(false);
+                voffsetCursor = getVerticalOffset(true);
+            }
+            canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
 
-        int extendedPaddingTop = getExtendedPaddingTop();
-        int extendedPaddingBottom = getExtendedPaddingBottom();
+            final int layoutDirection = getLayoutDirection();
+            final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+            if (isMarqueeFadeEnabled()) {
+                if (!mSingleLine && getLineCount() == 1 && canMarquee()
+                        && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
+                    final int width = mRight - mLeft;
+                    final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
+                    final float dx = mLayout.getLineRight(0) - (width - padding);
+                    canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+                }
 
-        final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
-        final int maxScrollY = mLayout.getHeight() - vspace;
+                if (mMarquee != null && mMarquee.isRunning()) {
+                    final float dx = -mMarquee.getScroll();
+                    canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+                }
+            }
 
-        float clipLeft = compoundPaddingLeft + scrollX;
-        float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
-        float clipRight = right - left - getCompoundPaddingRight() + scrollX;
-        float clipBottom = bottom - top + scrollY
-                - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
+            final int cursorOffsetVertical = voffsetCursor - voffsetText;
 
-        if (mShadowRadius != 0) {
-            clipLeft += Math.min(0, mShadowDx - mShadowRadius);
-            clipRight += Math.max(0, mShadowDx + mShadowRadius);
+            maybeUpdateHighlightPaths();
+            // If there is a gesture preview highlight, then the selection or cursor is not drawn.
+            Path highlight = hasGesturePreviewHighlight() ? null : getUpdatedHighlightPath();
+            if (mEditor != null) {
+                mEditor.onDraw(canvas, layout, mHighlightPaths, mHighlightPaints, highlight,
+                        mHighlightPaint, cursorOffsetVertical);
+            } else {
+                layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint,
+                        cursorOffsetVertical);
+            }
 
-            clipTop += Math.min(0, mShadowDy - mShadowRadius);
-            clipBottom += Math.max(0, mShadowDy + mShadowRadius);
-        }
-
-        canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
-
-        int voffsetText = 0;
-        int voffsetCursor = 0;
-
-        // translate in by our padding
-        /* shortcircuit calling getVerticaOffset() */
-        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
-            voffsetText = getVerticalOffset(false);
-            voffsetCursor = getVerticalOffset(true);
-        }
-        canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
-
-        final int layoutDirection = getLayoutDirection();
-        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
-        if (isMarqueeFadeEnabled()) {
-            if (!mSingleLine && getLineCount() == 1 && canMarquee()
-                    && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
-                final int width = mRight - mLeft;
-                final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
-                final float dx = mLayout.getLineRight(0) - (width - padding);
+            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+                final float dx = mMarquee.getGhostOffset();
                 canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+                layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint,
+                        cursorOffsetVertical);
             }
 
-            if (mMarquee != null && mMarquee.isRunning()) {
-                final float dx = -mMarquee.getScroll();
-                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
-            }
+            canvas.restore();
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
         }
-
-        final int cursorOffsetVertical = voffsetCursor - voffsetText;
-
-        maybeUpdateHighlightPaths();
-        // If there is a gesture preview highlight, then the selection or cursor is not drawn.
-        Path highlight = hasGesturePreviewHighlight() ? null : getUpdatedHighlightPath();
-        if (mEditor != null) {
-            mEditor.onDraw(canvas, layout, mHighlightPaths, mHighlightPaints, highlight,
-                    mHighlightPaint, cursorOffsetVertical);
-        } else {
-            layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint,
-                    cursorOffsetVertical);
-        }
-
-        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
-            final float dx = mMarquee.getGhostOffset();
-            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
-            layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint,
-                    cursorOffsetVertical);
-        }
-
-        canvas.restore();
     }
 
     @Override
@@ -11254,192 +11260,201 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
-        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
-        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "TextView.onMeasure");
+        try {
+            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 
-        int width;
-        int height;
+            int width;
+            int height;
 
-        BoringLayout.Metrics boring = UNKNOWN_BORING;
-        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
+            BoringLayout.Metrics boring = UNKNOWN_BORING;
+            BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
 
-        if (mTextDir == null) {
-            mTextDir = getTextDirectionHeuristic();
-        }
-
-        int des = -1;
-        boolean fromexisting = false;
-        final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
-                ?  (float) widthSize : Float.MAX_VALUE;
-
-        if (widthMode == MeasureSpec.EXACTLY) {
-            // Parent has told us how big to be. So be it.
-            width = widthSize;
-        } else {
-            if (mLayout != null && mEllipsize == null) {
-                des = desired(mLayout, mUseBoundsForWidth);
+            if (mTextDir == null) {
+                mTextDir = getTextDirectionHeuristic();
             }
 
-            if (des < 0) {
-                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
-                        mBoring);
-                if (boring != null) {
-                    mBoring = boring;
-                }
+            int des = -1;
+            boolean fromexisting = false;
+            final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
+                    ? (float) widthSize : Float.MAX_VALUE;
+
+            if (widthMode == MeasureSpec.EXACTLY) {
+                // Parent has told us how big to be. So be it.
+                width = widthSize;
             } else {
-                fromexisting = true;
-            }
+                if (mLayout != null && mEllipsize == null) {
+                    des = desired(mLayout, mUseBoundsForWidth);
+                }
 
-            if (boring == null || boring == UNKNOWN_BORING) {
                 if (des < 0) {
-                    des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
-                            mTransformed.length(), mTextPaint, mTextDir, widthLimit,
-                            mUseBoundsForWidth));
-                }
-                width = des;
-            } else {
-                if (mUseBoundsForWidth) {
-                    RectF bbox = boring.getDrawingBoundingBox();
-                    float rightMax = Math.max(bbox.right, boring.width);
-                    float leftMin = Math.min(bbox.left, 0);
-                    width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin));
-                } else {
-                    width = boring.width;
-                }
-            }
-
-            final Drawables dr = mDrawables;
-            if (dr != null) {
-                width = Math.max(width, dr.mDrawableWidthTop);
-                width = Math.max(width, dr.mDrawableWidthBottom);
-            }
-
-            if (mHint != null) {
-                int hintDes = -1;
-                int hintWidth;
-
-                if (mHintLayout != null && mEllipsize == null) {
-                    hintDes = desired(mHintLayout, mUseBoundsForWidth);
-                }
-
-                if (hintDes < 0) {
-                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
+                    boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
                             isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
-                            mHintBoring);
-                    if (hintBoring != null) {
-                        mHintBoring = hintBoring;
+                            mBoring);
+                    if (boring != null) {
+                        mBoring = boring;
                     }
+                } else {
+                    fromexisting = true;
                 }
 
-                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
-                    if (hintDes < 0) {
-                        hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
-                                mHint.length(), mTextPaint, mTextDir, widthLimit,
+                if (boring == null || boring == UNKNOWN_BORING) {
+                    if (des < 0) {
+                        des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
+                                mTransformed.length(), mTextPaint, mTextDir, widthLimit,
                                 mUseBoundsForWidth));
                     }
-                    hintWidth = hintDes;
+                    width = des;
                 } else {
-                    hintWidth = hintBoring.width;
+                    if (mUseBoundsForWidth) {
+                        RectF bbox = boring.getDrawingBoundingBox();
+                        float rightMax = Math.max(bbox.right, boring.width);
+                        float leftMin = Math.min(bbox.left, 0);
+                        width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin));
+                    } else {
+                        width = boring.width;
+                    }
                 }
 
-                if (hintWidth > width) {
-                    width = hintWidth;
+                final Drawables dr = mDrawables;
+                if (dr != null) {
+                    width = Math.max(width, dr.mDrawableWidthTop);
+                    width = Math.max(width, dr.mDrawableWidthBottom);
                 }
-            }
 
-            width += getCompoundPaddingLeft() + getCompoundPaddingRight();
+                if (mHint != null) {
+                    int hintDes = -1;
+                    int hintWidth;
 
-            if (mMaxWidthMode == EMS) {
-                width = Math.min(width, mMaxWidth * getLineHeight());
-            } else {
-                width = Math.min(width, mMaxWidth);
-            }
+                    if (mHintLayout != null && mEllipsize == null) {
+                        hintDes = desired(mHintLayout, mUseBoundsForWidth);
+                    }
 
-            if (mMinWidthMode == EMS) {
-                width = Math.max(width, mMinWidth * getLineHeight());
-            } else {
-                width = Math.max(width, mMinWidth);
-            }
+                    if (hintDes < 0) {
+                        hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
+                                isFallbackLineSpacingForBoringLayout(),
+                                getResolvedMinimumFontMetrics(),
+                                mHintBoring);
+                        if (hintBoring != null) {
+                            mHintBoring = hintBoring;
+                        }
+                    }
 
-            // Check against our minimum width
-            width = Math.max(width, getSuggestedMinimumWidth());
+                    if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
+                        if (hintDes < 0) {
+                            hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
+                                    mHint.length(), mTextPaint, mTextDir, widthLimit,
+                                    mUseBoundsForWidth));
+                        }
+                        hintWidth = hintDes;
+                    } else {
+                        hintWidth = hintBoring.width;
+                    }
 
-            if (widthMode == MeasureSpec.AT_MOST) {
-                width = Math.min(widthSize, width);
-            }
-        }
+                    if (hintWidth > width) {
+                        width = hintWidth;
+                    }
+                }
 
-        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
-        int unpaddedWidth = want;
+                width += getCompoundPaddingLeft() + getCompoundPaddingRight();
 
-        if (mHorizontallyScrolling) want = VERY_WIDE;
-
-        int hintWant = want;
-        int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
-
-        if (mLayout == null) {
-            makeNewLayout(want, hintWant, boring, hintBoring,
-                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
-        } else {
-            final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
-                    || (mLayout.getEllipsizedWidth()
-                            != width - getCompoundPaddingLeft() - getCompoundPaddingRight());
-
-            final boolean widthChanged = (mHint == null) && (mEllipsize == null)
-                    && (want > mLayout.getWidth())
-                    && (mLayout instanceof BoringLayout
-                            || (fromexisting && des >= 0 && des <= want));
-
-            final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
-
-            if (layoutChanged || maximumChanged) {
-                if (!maximumChanged && widthChanged) {
-                    mLayout.increaseWidthTo(want);
+                if (mMaxWidthMode == EMS) {
+                    width = Math.min(width, mMaxWidth * getLineHeight());
                 } else {
-                    makeNewLayout(want, hintWant, boring, hintBoring,
-                            width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+                    width = Math.min(width, mMaxWidth);
                 }
+
+                if (mMinWidthMode == EMS) {
+                    width = Math.max(width, mMinWidth * getLineHeight());
+                } else {
+                    width = Math.max(width, mMinWidth);
+                }
+
+                // Check against our minimum width
+                width = Math.max(width, getSuggestedMinimumWidth());
+
+                if (widthMode == MeasureSpec.AT_MOST) {
+                    width = Math.min(widthSize, width);
+                }
+            }
+
+            int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
+            int unpaddedWidth = want;
+
+            if (mHorizontallyScrolling) want = VERY_WIDE;
+
+            int hintWant = want;
+            int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
+
+            if (mLayout == null) {
+                makeNewLayout(want, hintWant, boring, hintBoring,
+                        width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
             } else {
-                // Nothing has changed
+                final boolean layoutChanged =
+                        (mLayout.getWidth() != want) || (hintWidth != hintWant)
+                                || (mLayout.getEllipsizedWidth()
+                                != width - getCompoundPaddingLeft() - getCompoundPaddingRight());
+
+                final boolean widthChanged = (mHint == null) && (mEllipsize == null)
+                        && (want > mLayout.getWidth())
+                        && (mLayout instanceof BoringLayout
+                        || (fromexisting && des >= 0 && des <= want));
+
+                final boolean maximumChanged =
+                        (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
+
+                if (layoutChanged || maximumChanged) {
+                    if (!maximumChanged && widthChanged) {
+                        mLayout.increaseWidthTo(want);
+                    } else {
+                        makeNewLayout(want, hintWant, boring, hintBoring,
+                                width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+                                false);
+                    }
+                } else {
+                    // Nothing has changed
+                }
             }
-        }
 
-        if (heightMode == MeasureSpec.EXACTLY) {
-            // Parent has told us how big to be. So be it.
-            height = heightSize;
-            mDesiredHeightAtMeasure = -1;
-        } else {
-            int desired = getDesiredHeight();
+            if (heightMode == MeasureSpec.EXACTLY) {
+                // Parent has told us how big to be. So be it.
+                height = heightSize;
+                mDesiredHeightAtMeasure = -1;
+            } else {
+                int desired = getDesiredHeight();
 
-            height = desired;
-            mDesiredHeightAtMeasure = desired;
+                height = desired;
+                mDesiredHeightAtMeasure = desired;
 
-            if (heightMode == MeasureSpec.AT_MOST) {
-                height = Math.min(desired, heightSize);
+                if (heightMode == MeasureSpec.AT_MOST) {
+                    height = Math.min(desired, heightSize);
+                }
             }
-        }
 
-        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
-        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
-            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
-        }
+            int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
+            if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
+                unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
+            }
 
-        /*
-         * We didn't let makeNewLayout() register to bring the cursor into view,
-         * so do it here if there is any possibility that it is needed.
-         */
-        if (mMovement != null
-                || mLayout.getWidth() > unpaddedWidth
-                || mLayout.getHeight() > unpaddedHeight) {
-            registerForPreDraw();
-        } else {
-            scrollTo(0, 0);
-        }
+            /*
+             * We didn't let makeNewLayout() register to bring the cursor into view,
+             * so do it here if there is any possibility that it is needed.
+             */
+            if (mMovement != null
+                    || mLayout.getWidth() > unpaddedWidth
+                    || mLayout.getHeight() > unpaddedHeight) {
+                registerForPreDraw();
+            } else {
+                scrollTo(0, 0);
+            }
 
-        setMeasuredDimension(width, height);
+            setMeasuredDimension(width, height);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+        }
     }
 
     /**
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 0f2dd10..2c21417 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -49,6 +49,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.os.BinderProxy;
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -1089,8 +1090,13 @@
         @Override
         public String toString() {
             final StringBuilder sb = new StringBuilder();
-            sb.append('{'); sb.append(mContainer);
-            sb.append(" m="); sb.append(modeToString(mMode));
+            sb.append('{');
+            if (mContainer != null && !(mContainer.asBinder() instanceof BinderProxy)) {
+                // Only log the token if it is not a binder proxy and has additional container info
+                sb.append(mContainer);
+                sb.append(" ");
+            }
+            sb.append("m="); sb.append(modeToString(mMode));
             sb.append(" f="); sb.append(flagsToString(mFlags));
             if (mParent != null) {
                 sb.append(" p="); sb.append(mParent);
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 3fe63ab..a88a172 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -1120,8 +1120,8 @@
     @NonNull
     public String toString() {
         return "WindowContainerTransaction {"
-                + " changes = " + mChanges
-                + " hops = " + mHierarchyOps
+                + " changes= " + mChanges
+                + " hops= " + mHierarchyOps
                 + " errorCallbackToken=" + mErrorCallbackToken
                 + " taskFragmentOrganizer=" + mTaskFragmentOrganizer
                 + " }";
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 4f924a82..ff69610 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -418,6 +418,17 @@
 }
 
 flag {
+  name: "record_task_snapshots_before_shutdown"
+  namespace: "windowing_frontend"
+  description: "Record task snapshots before shutdown"
+  bug: "376821232"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "predictive_back_three_button_nav"
     namespace: "systemui"
     description: "Enable Predictive Back Animation for 3-button-nav"
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index a1c987f..eb682df 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -676,15 +676,30 @@
         return internMap.get(string);
     }
 
+    protected boolean validateGroups(ILogger logger, String[] groups) {
+        for (int i = 0; i < groups.length; i++) {
+            String group = groups[i];
+            IProtoLogGroup g = mLogGroups.get(group);
+            if (g == null) {
+                logger.log("No IProtoLogGroup named " + group);
+                return false;
+            }
+        }
+        return true;
+    }
+
     private int setTextLogging(boolean value, ILogger logger, String... groups) {
+        if (!validateGroups(logger, groups)) {
+            return -1;
+        }
+
         for (int i = 0; i < groups.length; i++) {
             String group = groups[i];
             IProtoLogGroup g = mLogGroups.get(group);
             if (g != null) {
                 g.setLogToLogcat(value);
             } else {
-                logger.log("No IProtoLogGroup named " + group);
-                return -1;
+                throw new RuntimeException("No IProtoLogGroup named " + group);
             }
         }
 
diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
index 70d148a..967a5ed 100644
--- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
@@ -113,6 +113,10 @@
      */
     @Override
     public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        if (!validateGroups(logger, groups)) {
+            return -1;
+        }
+
         mViewerConfigReader.loadViewerConfig(groups, logger);
         return super.startLoggingToLogcat(groups, logger);
     }
@@ -125,8 +129,19 @@
      */
     @Override
     public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        if (!validateGroups(logger, groups)) {
+            return -1;
+        }
+
+        var status = super.stopLoggingToLogcat(groups, logger);
+
+        if (status != 0) {
+            throw new RuntimeException("Failed to stop logging to logcat");
+        }
+
+        // If we successfully disabled logging, unload the viewer config.
         mViewerConfigReader.unloadViewerConfig(groups, logger);
-        return super.stopLoggingToLogcat(groups, logger);
+        return status;
     }
 
     @Deprecated
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 3303d87..8df3f2a 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -88,15 +88,19 @@
     /** @hide */
     public static class CompatIdsForTest {
         // Enabled by default
+        /** Used for testing */
         @ChangeId
         public static final long TEST_COMPAT_ID_1 = 368131859L;
 
+        /** Used for testing */
         @Disabled
         @ChangeId public static final long TEST_COMPAT_ID_2 = 368131701L;
 
+        /** Used for testing */
         @EnabledAfter(targetSdkVersion = S)
         @ChangeId public static final long TEST_COMPAT_ID_3 = 368131659L;
 
+        /** Used for testing */
         @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE)
         @ChangeId public static final long TEST_COMPAT_ID_4 = 368132057L;
     }
diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp
index bb4084e..f64dec8 100644
--- a/core/jni/android_hardware_OverlayProperties.cpp
+++ b/core/jni/android_hardware_OverlayProperties.cpp
@@ -106,7 +106,7 @@
                                                                         jlong nativeObject) {
     gui::OverlayProperties* overlayProperties =
             reinterpret_cast<gui::OverlayProperties*>(nativeObject);
-    if (overlayProperties->lutProperties.has_value()) {
+    if (!overlayProperties || !overlayProperties->lutProperties) {
         return NULL;
     }
     auto& lutProperties = overlayProperties->lutProperties.value();
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 49191ee..7ef7829 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -33,6 +33,7 @@
 
 #include <algorithm>
 #include <array>
+#include <cctype>
 #include <cstring>
 #include <limits>
 #include <memory>
@@ -1008,6 +1009,8 @@
                 }
             }
             if ((mode&PROC_OUT_STRING) != 0 && di < NS) {
+                std::replace_if(buffer+start, buffer+end,
+                                [](unsigned char c){ return !std::isprint(c); }, '?');
                 jstring str = env->NewStringUTF(buffer+start);
                 env->SetObjectArrayElement(outStrings, di, str);
             }
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 82b463e..1925b3a 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -758,54 +758,64 @@
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
     SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
 
-    ScopedIntArrayRW joffsets(env, joffsetArray);
-    if (joffsets.get() == nullptr) {
-        jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray");
-        return;
-    }
-    ScopedIntArrayRW jdimensions(env, jdimensionArray);
-    if (jdimensions.get() == nullptr) {
-        jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray");
-        return;
-    }
-    ScopedIntArrayRW jsizes(env, jsizeArray);
-    if (jsizes.get() == nullptr) {
-        jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray");
-        return;
-    }
-    ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray);
-    if (jsamplingKeys.get() == nullptr) {
-        jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray");
-        return;
-    }
+    std::vector<int32_t> offsets;
+    std::vector<int32_t> dimensions;
+    std::vector<int32_t> sizes;
+    std::vector<int32_t> samplingKeys;
+    int32_t fd = -1;
 
-    jsize numLuts = env->GetArrayLength(jdimensionArray);
-    std::vector<int32_t> offsets(joffsets.get(), joffsets.get() + numLuts);
-    std::vector<int32_t> dimensions(jdimensions.get(), jdimensions.get() + numLuts);
-    std::vector<int32_t> sizes(jsizes.get(), jsizes.get() + numLuts);
-    std::vector<int32_t> samplingKeys(jsamplingKeys.get(), jsamplingKeys.get() + numLuts);
+    if (jdimensionArray) {
+        jsize numLuts = env->GetArrayLength(jdimensionArray);
+        ScopedIntArrayRW joffsets(env, joffsetArray);
+        if (joffsets.get() == nullptr) {
+            jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray");
+            return;
+        }
+        ScopedIntArrayRW jdimensions(env, jdimensionArray);
+        if (jdimensions.get() == nullptr) {
+            jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray");
+            return;
+        }
+        ScopedIntArrayRW jsizes(env, jsizeArray);
+        if (jsizes.get() == nullptr) {
+            jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray");
+            return;
+        }
+        ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray);
+        if (jsamplingKeys.get() == nullptr) {
+            jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray");
+            return;
+        }
 
-    ScopedFloatArrayRW jbuffers(env, jbufferArray);
-    if (jbuffers.get() == nullptr) {
-        jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray");
-        return;
-    }
+        if (numLuts > 0) {
+            offsets = std::vector<int32_t>(joffsets.get(), joffsets.get() + numLuts);
+            dimensions = std::vector<int32_t>(jdimensions.get(), jdimensions.get() + numLuts);
+            sizes = std::vector<int32_t>(jsizes.get(), jsizes.get() + numLuts);
+            samplingKeys = std::vector<int32_t>(jsamplingKeys.get(), jsamplingKeys.get() + numLuts);
 
-    // create the shared memory and copy jbuffers
-    size_t bufferSize = jbuffers.size() * sizeof(float);
-    int32_t fd = ashmem_create_region("lut_shread_mem", bufferSize);
-    if (fd < 0) {
-        jniThrowRuntimeException(env, "ashmem_create_region() failed");
-        return;
+            ScopedFloatArrayRW jbuffers(env, jbufferArray);
+            if (jbuffers.get() == nullptr) {
+                jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray");
+                return;
+            }
+
+            // create the shared memory and copy jbuffers
+            size_t bufferSize = jbuffers.size() * sizeof(float);
+            fd = ashmem_create_region("lut_shared_mem", bufferSize);
+            if (fd < 0) {
+                jniThrowRuntimeException(env, "ashmem_create_region() failed");
+                return;
+            }
+            void* ptr = mmap(nullptr, bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+            if (ptr == MAP_FAILED) {
+                jniThrowRuntimeException(env, "Failed to map the shared memory");
+                return;
+            }
+            memcpy(ptr, jbuffers.get(), bufferSize);
+            // unmap
+            munmap(ptr, bufferSize);
+        }
     }
-    void* ptr = mmap(nullptr, bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
-    if (ptr == MAP_FAILED) {
-        jniThrowRuntimeException(env, "Failed to map the shared memory");
-        return;
-    }
-    memcpy(ptr, jbuffers.get(), bufferSize);
-    // unmap
-    munmap(ptr, bufferSize);
 
     transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys);
 }
@@ -1332,8 +1342,9 @@
     }
 }
 
-static jobject convertDeviceProductInfoToJavaObject(
-        JNIEnv* env, const std::optional<DeviceProductInfo>& info) {
+static jobject convertDeviceProductInfoToJavaObject(JNIEnv* env,
+                                                    const std::optional<DeviceProductInfo>& info,
+                                                    bool isInternal) {
     using ModelYear = android::DeviceProductInfo::ModelYear;
     using ManufactureYear = android::DeviceProductInfo::ManufactureYear;
     using ManufactureWeekAndYear = android::DeviceProductInfo::ManufactureWeekAndYear;
@@ -1368,7 +1379,8 @@
     // Section 8.7 - Physical Address of HDMI Specification Version 1.3a
     using android::hardware::display::IDeviceProductInfoConstants;
     if (info->relativeAddress.size() != 4) {
-        connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN;
+        connectionToSinkType = isInternal ? IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN
+                                          : IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN;
     } else if (info->relativeAddress[0] == 0) {
         connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN;
     } else if (info->relativeAddress[1] == 0) {
@@ -1390,12 +1402,14 @@
 
     jobject object =
             env->NewObject(gStaticDisplayInfoClassInfo.clazz, gStaticDisplayInfoClassInfo.ctor);
-    env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal,
-                         info.connectionType == ui::DisplayConnectionType::Internal);
+
+    const bool isInternal = info.connectionType == ui::DisplayConnectionType::Internal;
+    env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal, isInternal);
     env->SetFloatField(object, gStaticDisplayInfoClassInfo.density, info.density);
     env->SetBooleanField(object, gStaticDisplayInfoClassInfo.secure, info.secure);
     env->SetObjectField(object, gStaticDisplayInfoClassInfo.deviceProductInfo,
-                        convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo));
+                        convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo,
+                                                             isInternal));
     env->SetIntField(object, gStaticDisplayInfoClassInfo.installOrientation,
                      static_cast<uint32_t>(info.installOrientation));
     return object;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3e0c120..95d07df 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1080,6 +1080,52 @@
     <permission android:name="android.permission.SATELLITE_COMMUNICATION"
                 android:protectionLevel="role|signature|privileged" />
 
+    <!-- ================================== -->
+    <!-- Permissions associated with picture and sound profiles  -->
+    <!-- ================================== -->
+    <eat-comment />
+
+    <!-- @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES)
+         Allows an app to apply a {@link MediaQualityManager.PictureProfile} to a layer via
+         {@link MediaCodec.PARAMETER_KEY_PICTURE_PROFILE} and, additionally, system apps via
+         {@link SurfaceControl.Transaction#setPictureProfileHandle}.
+         -->
+    <permission android:name="android.permission.APPLY_PICTURE_PROFILE"
+        android:protectionLevel="normal"
+        android:featureFlag="android.media.tv.flags.apply_picture_profiles"/>
+
+    <!-- @hide
+         Allows MediaQualityService to observe any {@link MediaQualityManager.PictureProfile}
+         applied to any layer in the system by apps via
+         {@link MediaCodec.PARAMETER_KEY_PICTURE_PROFILE} and by system apps via
+         {@link SurfaceControl.Transaction#setPictureProfileHandle}.
+         -->
+    <permission android:name="android.permission.OBSERVE_PICTURE_PROFILES"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.media.tv.flags.apply_picture_profiles"/>
+
+    <!--
+        @SystemApi
+        @FlaggedApi("android.media.tv.flags.media_quality_fw")
+        Allows an application to access its picture profile from the media quality database.
+        <p> Protection level: signature|privileged|vendor privileged
+        @hide
+    -->
+    <permission android:name="android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE"
+                android:protectionLevel="signature|privileged|vendorPrivileged"
+                android:featureFlag="android.media.tv.flags.media_quality_fw"/>
+
+    <!--
+        @SystemApi
+        @FlaggedApi("android.media.tv.flags.media_quality_fw")
+        Allows an application to access its sound profile from the media quality database.
+        <p> Protection level: signature|privileged|vendor privileged
+        @hide
+    -->
+    <permission android:name="android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE"
+                android:protectionLevel="signature|privileged|vendorPrivileged"
+                android:featureFlag="android.media.tv.flags.media_quality_fw"/>
+
     <!-- ====================================================================== -->
     <!-- Permissions for accessing external storage                             -->
     <!-- ====================================================================== -->
@@ -4144,6 +4190,37 @@
     <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
+         @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.READ_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature|privileged" />
+    <uses-permission android:name="android.permission.READ_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+    <!-- Allows an application to change the state of the ForensicService
+         @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature|privileged" />
+    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+    <!-- Must be required by any ForensicEventTransportService 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"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"
+        android:featureFlag="android.security.afl_api"/>
+
     <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
     <permission android:name="android.permission.PROVISION_DEMO_DEVICE"
                 android:protectionLevel="signature|setup|knownSigner"
@@ -4991,16 +5068,16 @@
         android:protectionLevel="signature|privileged|role"
         android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
 
-    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST)
+    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_WRITE_SYSTEM_PREFERENCE_PERMISSION_ENABLED)
          Allows an application to access the Settings Preference services to write settings
          values exposed by the system Settings app and system apps that contribute settings surfaced
          in the Settings app.
          <p>This allows the calling application to write settings values
          through the host application, agnostic of underlying storage.
-         <p>Protection Level: signature|privileged|appop - appop to be added in followup -->
+         <p>Protection Level: signature|privileged|appop -->
     <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES"
-        android:protectionLevel="signature|privileged"
-        android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
+        android:protectionLevel="signature|privileged|appop"
+        android:featureFlag="com.android.settingslib.flags.write_system_preference_permission_enabled" />
 
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
@@ -8565,27 +8642,6 @@
     <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
                 android:protectionLevel="signature"/>
 
-    <!--
-        @SystemApi
-        @FlaggedApi("android.media.tv.flags.media_quality_fw")
-        Allows an application to access its picture profile from the media quality database.
-        <p> Protection level: signature|privileged|vendor privileged
-        @hide
-    -->
-    <permission android:name="android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE"
-                android:protectionLevel="signature|privileged|vendorPrivileged"
-                android:featureFlag="android.media.tv.flags.media_quality_fw"/>
-
-    <!--
-        @SystemApi
-        @FlaggedApi("android.media.tv.flags.media_quality_fw")
-        Allows an application to access its sound profile from the media quality database.
-        <p> Protection level: signature|privileged|vendor privileged
-        @hide
-    -->
-    <permission android:name="android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE"
-                android:protectionLevel="signature|privileged|vendorPrivileged"
-                android:featureFlag="android.media.tv.flags.media_quality_fw"/>
     <!-- @SystemApi
         @FlaggedApi("android.content.pm.verification_service")
         Allows app to be the verification agent to verify packages.
diff --git a/core/res/res/layout/list_content_simple.xml b/core/res/res/layout/list_content_simple.xml
index 6f9f1e0..961668e 100644
--- a/core/res/res/layout/list_content_simple.xml
+++ b/core/res/res/layout/list_content_simple.xml
@@ -20,5 +20,6 @@
 <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list"
     android:layout_width="match_parent" 
     android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
     android:drawSelectorOnTop="false"
     />
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 41dec37..7ef5394 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1855,13 +1855,23 @@
          {@link android.R.styleable#AndroidManifestProcess process} tag, or to an
          {@link android.R.styleable#AndroidManifestApplication application} tag (to supply
          a default setting for all application components). -->
-    <attr name="memtagMode">
+     <attr name="memtagMode">
        <enum name="default" value="-1" />
        <enum name="off" value="0" />
        <enum name="async" value="1" />
        <enum name="sync" value="2" />
     </attr>
 
+    <!-- This attribute will be used to override app compatibility mode on 16 KB devices.
+         If set to enabled, Natives lib will be extracted from APK if they are not page aligned on
+         16 KB device. 4 KB natives libs will be loaded app-compat mode if they are eligible.
+         @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
+    <attr name="pageSizeCompat">
+        <enum name="enabled" value="5" />
+        <enum name="disabled" value="6" />
+    </attr>
+
+
     <!-- Attribution tag to be used for permission sub-attribution if a
       permission is checked in  {@link android.content.Context#sendBroadcast(Intent, String)}.
       Multiple tags can be specified separated by '|'.
@@ -2212,6 +2222,9 @@
 
         <attr name="memtagMode" />
 
+        <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
+        <attr name="pageSizeCompat" />
+
         <!-- If {@code true} enables automatic zero initialization of all native heap
              allocations. -->
         <attr name="nativeHeapZeroInitialized" format="boolean" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 969ee2e..7799ff9 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6567,7 +6567,7 @@
     </string-array>
 
     <!-- the number of the max cached processes in the system. -->
-    <integer name="config_customizedMaxCachedProcesses">32</integer>
+    <integer name="config_customizedMaxCachedProcesses">1024</integer>
 
     <!-- Whether this device should support taking app snapshots on closure -->
     <bool name="config_disableTaskSnapshots">false</bool>
@@ -7212,8 +7212,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 backup transport. -->
-    <string name="config_forensicBackupTransport" translatable="false"></string>
+    <!-- The name of the service for forensic event transport. -->
+    <string name="config_forensicEventTransport" 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 31e9913..4ec27a3 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -318,6 +318,12 @@
     <bool name="config_oem_enabled_satellite_access_allow">true</bool>
     <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" />
 
+    <!-- Whether the satellite modem support concurrent TN scanning while device is in
+         NTN mode.
+         -->
+    <bool name="config_satellite_modem_support_concurrent_tn_scanning">true</bool>
+    <java-symbol type="bool" name="config_satellite_modem_support_concurrent_tn_scanning" />
+
     <!-- The time duration in seconds which is used to decide whether the Location returned from
          LocationManager#getLastKnownLocation is fresh.
 
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index b6436d0..a0bf89d 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -131,6 +131,8 @@
     <public name="alternateLauncherIcons"/>
     <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
     <public name="alternateLauncherLabels"/>
+    <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
+    <public name="pageSizeCompat" />
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01b60000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9dd3027..7fe0912 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5679,8 +5679,8 @@
   <java-symbol type="string" name="identity_check_settings_action" />
   <java-symbol type="string" name="identity_check_settings_package_name" />
 
-  <!-- Forensic backup transport -->
-  <java-symbol type="string" name="config_forensicBackupTransport" />
+  <!-- Forensic event transport -->
+  <java-symbol type="string" name="config_forensicEventTransport" />
 
   <!-- Fingerprint screen off unlock config -->
   <java-symbol type="bool" name="config_screen_off_udfps_enabled" />
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
index 9dd196d..f62d420 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
@@ -507,7 +507,8 @@
                 + "prefix: supportedHandwritingGestureTypes=(none)\n"
                 + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n"
                 + "prefix: isStylusHandwritingEnabled=false\n"
-                + "prefix: contentMimeTypes=null\n");
+                + "prefix: contentMimeTypes=null\n"
+                + "prefix: writingToolsEnabled=true\n");
     }
 
     @Test
@@ -539,6 +540,7 @@
         info.hintLocales = LocaleList.forLanguageTags("en,es,zh");
         info.contentMimeTypes = new String[] {"image/png"};
         info.targetInputMethodUser = UserHandle.of(10);
+        info.setWritingToolsEnabled(false);
         final StringBuilder sb = new StringBuilder();
         info.dump(new StringBuilderPrinter(sb), "prefix2: ");
         assertThat(sb.toString()).isEqualTo(
@@ -555,7 +557,8 @@
                         + "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n"
                         + "prefix2: isStylusHandwritingEnabled=" + isStylusHandwritingEnabled + "\n"
                         + "prefix2: contentMimeTypes=[image/png]\n"
-                        + "prefix2: targetInputMethodUserId=10\n");
+                        + "prefix2: targetInputMethodUserId=10\n"
+                        + "prefix2: writingToolsEnabled=false\n");
     }
 
     @Test
@@ -576,7 +579,8 @@
                         + "prefix: supportedHandwritingGestureTypes=(none)\n"
                         + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n"
                         + "prefix: isStylusHandwritingEnabled=false\n"
-                        + "prefix: contentMimeTypes=null\n");
+                        + "prefix: contentMimeTypes=null\n"
+                        + "prefix: writingToolsEnabled=true\n");
     }
 
     @Test
@@ -621,4 +625,9 @@
         infoCopy.extras.putString("testKey2", "testValue");
         assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy));
     }
+
+    @Test
+    public void testWritingToolsEnabledbyDefault() {
+        assertTrue(TEST_EDITOR_INFO.isWritingToolsEnabled());
+    }
 }
diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS
index 6149382..4620cb8 100644
--- a/core/tests/coretests/src/android/os/OWNERS
+++ b/core/tests/coretests/src/android/os/OWNERS
@@ -12,3 +12,6 @@
 
 # Caching
 per-file IpcDataCache* = file:/PERFORMANCE_OWNERS
+
+# RemoteCallbackList
+per-file RemoteCallbackListTest.java = shayba@google.com
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 541ca60..fea7cb4 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -93,6 +93,10 @@
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
     </privapp-permissions>
 
+    <privapp-permissions package="com.android.media.quality">
+        <permission name="android.permission.OBSERVE_PICTURE_PROFILES"/>
+    </privapp-permissions>
+
     <privapp-permissions package="com.android.mtp">
         <permission name="android.permission.ACCESS_MTP"/>
         <permission name="android.permission.MANAGE_USB"/>
@@ -262,6 +266,8 @@
         <!-- BLUETOOTH_PRIVILEGED is needed for test only -->
         <permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
         <permission name="android.permission.BIND_APPWIDGET"/>
+        <!-- Needed for CTS tests only -->
+        <permission name="android.permission.OBSERVE_PICTURE_PROFILES"/>
         <permission name="android.permission.CHANGE_APP_IDLE_STATE"/>
         <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
         <permission name="android.permission.CHANGE_CONFIGURATION"/>
@@ -597,6 +603,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" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/graphics/java/android/graphics/BlendMode.java b/graphics/java/android/graphics/BlendMode.java
index 5c294aa..c6ae680 100644
--- a/graphics/java/android/graphics/BlendMode.java
+++ b/graphics/java/android/graphics/BlendMode.java
@@ -571,10 +571,10 @@
     }
 
     @NonNull
-    private final Xfermode mXfermode;
+    private final PorterDuffXfermode mXfermode;
 
     BlendMode(int mode) {
-        mXfermode = new Xfermode();
+        mXfermode = new PorterDuffXfermode();
         mXfermode.porterDuffMode = mode;
     }
 
@@ -582,7 +582,7 @@
      * @hide
      */
     @NonNull
-    public Xfermode getXfermode() {
+    public PorterDuffXfermode getXfermode() {
         return mXfermode;
     }
 }
diff --git a/graphics/java/android/graphics/ComposeShader.java b/graphics/java/android/graphics/ComposeShader.java
index 977aeaa..e714568 100644
--- a/graphics/java/android/graphics/ComposeShader.java
+++ b/graphics/java/android/graphics/ComposeShader.java
@@ -40,9 +40,12 @@
      * @param mode     The mode that combines the colors from the two shaders. If mode
      *                 is null, then SRC_OVER is assumed.
      */
+    //TODO(358126864): allow a ComposeShader to accept a RuntimeXfermode
     @Deprecated
     public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode) {
-        this(shaderA, shaderB, mode.porterDuffMode);
+        this(shaderA, shaderB,
+                mode instanceof PorterDuffXfermode ? ((PorterDuffXfermode) mode).porterDuffMode
+                : BlendMode.SRC_OVER.getXfermode().porterDuffMode);
     }
 
     /**
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 56bb0f0..2c166c3 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -71,6 +71,7 @@
     private long mNativePaint;
     private long mNativeShader;
     private long mNativeColorFilter;
+    private long mNativeXfermode;
 
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
@@ -735,6 +736,7 @@
         mPathEffect = null;
         mShader = null;
         mNativeShader = 0;
+        mNativeXfermode = 0;
         mTypeface = null;
         mXfermode = null;
 
@@ -780,6 +782,7 @@
         mNativeShader = paint.mNativeShader;
         mTypeface = paint.mTypeface;
         mXfermode = paint.mXfermode;
+        mNativeXfermode = paint.mNativeXfermode;
 
         mHasCompatScaling = paint.mHasCompatScaling;
         mCompatScaling = paint.mCompatScaling;
@@ -815,7 +818,7 @@
      *
      * Note: Although this method is |synchronized|, this is simply so it
      * is not thread-hostile to multiple threads calling this method. It
-     * is still unsafe to attempt to change the Shader/ColorFilter while
+     * is still unsafe to attempt to change the Shader/ColorFilter/Xfermode while
      * another thread attempts to access the native object.
      *
      * @hide
@@ -833,6 +836,13 @@
             mNativeColorFilter = newNativeColorFilter;
             nSetColorFilter(mNativePaint, mNativeColorFilter);
         }
+        if (mXfermode instanceof RuntimeXfermode) {
+            long newNativeXfermode = ((RuntimeXfermode) mXfermode).createNativeInstance();
+            if (newNativeXfermode != mNativeXfermode) {
+                mNativeXfermode = newNativeXfermode;
+                nSetXfermode(mNativePaint, mNativeXfermode);
+            }
+        }
         return mNativePaint;
     }
 
@@ -1427,16 +1437,17 @@
     }
 
     /**
-     * Get the paint's blend mode object.
+     * Get the paint's blend mode object. Will return null if there is a Xfermode applied that
+     * cannot be represented by a blend mode (i.e. a custom {@code RuntimeXfermode}
      *
      * @return the paint's blend mode (or null)
      */
     @Nullable
     public BlendMode getBlendMode() {
-        if (mXfermode == null) {
+        if (mXfermode == null || !(mXfermode instanceof PorterDuffXfermode)) {
             return null;
         } else {
-            return BlendMode.fromValue(mXfermode.porterDuffMode);
+            return BlendMode.fromValue(((PorterDuffXfermode) mXfermode).porterDuffMode);
         }
     }
 
@@ -1459,8 +1470,15 @@
 
     @Nullable
     private Xfermode installXfermode(Xfermode xfermode) {
-        int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
-        int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
+        if (xfermode instanceof RuntimeXfermode) {
+            mXfermode = xfermode;
+            nSetXfermode(mNativePaint, ((RuntimeXfermode) xfermode).createNativeInstance());
+            return xfermode;
+        }
+        int newMode = (xfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) xfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
+        int curMode = (mXfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) mXfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
         if (newMode != curMode) {
             nSetXfermode(mNativePaint, newMode);
         }
@@ -3823,6 +3841,8 @@
     @CriticalNative
     private static native void nSetXfermode(long paintPtr, int xfermode);
     @CriticalNative
+    private static native void nSetXfermode(long paintPtr, long xfermodePtr);
+    @CriticalNative
     private static native long nSetPathEffect(long paintPtr, long effect);
     @CriticalNative
     private static native long nSetMaskFilter(long paintPtr, long maskfilter);
diff --git a/graphics/java/android/graphics/PorterDuffXfermode.java b/graphics/java/android/graphics/PorterDuffXfermode.java
index ff9ff8b..83d0507 100644
--- a/graphics/java/android/graphics/PorterDuffXfermode.java
+++ b/graphics/java/android/graphics/PorterDuffXfermode.java
@@ -29,6 +29,9 @@
      *
      * @param mode           The porter-duff mode that is applied
      */
+    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
+    int porterDuffMode = DEFAULT;
+    PorterDuffXfermode() {}
     public PorterDuffXfermode(PorterDuff.Mode mode) {
         porterDuffMode = mode.nativeInt;
     }
diff --git a/graphics/java/android/graphics/RuntimeXfermode.java b/graphics/java/android/graphics/RuntimeXfermode.java
new file mode 100644
index 0000000..f5a6568
--- /dev/null
+++ b/graphics/java/android/graphics/RuntimeXfermode.java
@@ -0,0 +1,312 @@
+/*
+ * 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.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.graphics.hwui.flags.Flags;
+
+import libcore.util.NativeAllocationRegistry;
+
+
+/**
+ * <p>A {@link RuntimeXfermode} calculates a per-pixel color based on the output of a user
+ *  * defined Android Graphics Shading Language (AGSL) function.</p>
+ *
+ * <p>This AGSL function takes in two input colors to be operated on. These colors are in sRGB
+ *  * and the output is also interpreted as sRGB. The AGSL function signature expects a single input
+ *  * of color (packed as a half4 or float4 or vec4).</p>
+ *
+ * <pre class="prettyprint">
+ * vec4 main(half4 src, half4 dst);
+ * </pre>
+ */
+@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS)
+public class RuntimeXfermode extends Xfermode {
+
+    private static class NoImagePreloadHolder {
+        public static final NativeAllocationRegistry sRegistry =
+                NativeAllocationRegistry.createMalloced(
+                        RuntimeXfermode.class.getClassLoader(), nativeGetFinalizer());
+    }
+
+    private long mBuilderNativeInstance;
+
+    /**
+     * Creates a new RuntimeBlender.
+     *
+     * @param agsl The text of AGSL color filter program to run.
+     */
+    public RuntimeXfermode(@NonNull String agsl) {
+        if (agsl == null) {
+            throw new NullPointerException("RuntimeShader requires a non-null AGSL string");
+        }
+        mBuilderNativeInstance = nativeCreateBlenderBuilder(agsl);
+        RuntimeXfermode.NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+                this, mBuilderNativeInstance);
+    }
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorInt int color) {
+        setUniform(uniformName, Color.valueOf(color).getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorLong long color) {
+        Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
+        if (color == null) {
+            throw new NullPointerException("The color parameter must not be null");
+        }
+        Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float or
+     * float[1] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value) {
+        setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec2 or
+     * float[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
+        setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec3 or
+     * float[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3) {
+        setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
+
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec4 or
+     * float[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4) {
+        setFloatUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float
+     * (for N=1), vecN, or float[N] where N is the length of the values param then an
+     * IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
+        setUniform(uniformName, values, false);
+    }
+
+    private void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values, isColor);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int or int[1]
+     * then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value) {
+        setIntUniform(uniformName, value, 0, 0, 0, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec2 or
+     * int[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
+        setIntUniform(uniformName, value1, value2, 0, 0, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec3 or
+     * int[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
+        setIntUniform(uniformName, value1, value2, value3, 0, 3);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec4 or
+     * int[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2,
+            int value3, int value4) {
+        setIntUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int (for N=1),
+     * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values);
+    }
+
+    private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3,
+            int value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    /**
+     * Assigns the uniform shader to the provided shader parameter.  If the shader program does not
+     * have a uniform shader with that name then an IllegalArgumentException is thrown.
+     *
+     * @param shaderName name matching the uniform declared in the AGSL program
+     * @param shader shader passed into the AGSL program for sampling
+     */
+    public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) {
+        if (shaderName == null) {
+            throw new NullPointerException("The shaderName parameter must not be null");
+        }
+        if (shader == null) {
+            throw new NullPointerException("The shader parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, shaderName, shader.getNativeInstance());
+    }
+
+    /**
+     * Assigns the uniform color filter to the provided color filter parameter.  If the shader
+     * program does not have a uniform color filter with that name then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param filterName name matching the uniform declared in the AGSL program
+     * @param colorFilter filter passed into the AGSL program for sampling
+     */
+    public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) {
+        if (filterName == null) {
+            throw new NullPointerException("The filterName parameter must not be null");
+        }
+        if (colorFilter == null) {
+            throw new NullPointerException("The colorFilter parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, filterName, colorFilter.getNativeInstance());
+    }
+
+    /** @hide */
+    public long createNativeInstance() {
+        return nativeCreateNativeInstance(mBuilderNativeInstance);
+    }
+
+    /** @hide */
+    private static native long nativeGetFinalizer();
+    private static native long nativeCreateBlenderBuilder(String agsl);
+    private static native long nativeCreateNativeInstance(long builder);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float[] uniforms, boolean isColor);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float value1, float value2, float value3,
+            float value4, int count);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int[] uniforms);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int value1, int value2, int value3,
+            int value4, int count);
+    private static native void nativeUpdateChild(long builder, String childName, long child);
+
+}
diff --git a/graphics/java/android/graphics/Xfermode.java b/graphics/java/android/graphics/Xfermode.java
index 6bb22a1..fb689e4 100644
--- a/graphics/java/android/graphics/Xfermode.java
+++ b/graphics/java/android/graphics/Xfermode.java
@@ -21,9 +21,6 @@
 
 package android.graphics;
 
-import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Build;
-
 /**
  * Xfermode is the base class for objects that are called to implement custom
  * "transfer-modes" in the drawing pipeline. The static function Create(Modes)
@@ -31,8 +28,4 @@
  * specified in the Modes enum. When an Xfermode is assigned to a Paint, then
  * objects drawn with that paint have the xfermode applied.
  */
-public class Xfermode {
-    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    int porterDuffMode = DEFAULT;
-}
+public class Xfermode {}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index 03e0ab0..4300e84 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -168,6 +168,16 @@
     }
 
     /**
+     * @return The task info for the task in this group with the given {@code taskId}.
+     */
+    @Nullable
+    public TaskInfo getTaskById(int taskId) {
+        return mTasks.stream()
+                .filter(task -> task.taskId == taskId)
+                .findFirst().orElse(null);
+    }
+
+    /**
      * Get all {@link RecentTaskInfo}s grouped together.
      */
     @NonNull
@@ -176,6 +186,14 @@
     }
 
     /**
+     * @return Whether this grouped task contains a task with the given {@code taskId}.
+     */
+    public boolean containsTask(int taskId) {
+        return mTasks.stream()
+                .anyMatch((task -> task.taskId == taskId));
+    }
+
+    /**
      * Return {@link SplitBounds} if this is a split screen entry or {@code null}
      */
     @Nullable
@@ -249,9 +267,10 @@
             return null;
         }
         return "id=" + taskInfo.taskId
-                + " baseIntent=" + (taskInfo.baseIntent != null
-                        ? taskInfo.baseIntent.getComponent()
-                        : "null")
+                + " baseIntent=" +
+                        (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
+                                ? taskInfo.baseIntent.getComponent().flattenToString()
+                                : "null")
                 + " winMode=" + WindowConfiguration.windowingModeToString(
                         taskInfo.getWindowingMode());
     }
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 77e041e..e455985 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
@@ -1024,10 +1024,13 @@
     @WMSingleton
     @Provides
     static TaskStackTransitionObserver provideTaskStackTransitionObserver(
-            Lazy<Transitions> transitions,
-            ShellInit shellInit
+            ShellInit shellInit,
+            Lazy<ShellTaskOrganizer> shellTaskOrganizer,
+            ShellCommandHandler shellCommandHandler,
+            Lazy<Transitions> transitions
     ) {
-        return new TaskStackTransitionObserver(transitions, shellInit);
+        return new TaskStackTransitionObserver(shellInit, shellTaskOrganizer, shellCommandHandler,
+                transitions);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 2001f97..82c2ebc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -23,6 +23,7 @@
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
 import android.window.DesktopModeFlags
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
@@ -95,7 +96,7 @@
     fun startLaunchTransition(
         @WindowManager.TransitionType transitionType: Int,
         wct: WindowContainerTransaction,
-        taskId: Int,
+        taskId: Int?,
         minimizingTaskId: Int? = null,
         exitingImmersiveTask: Int? = null,
     ): IBinder {
@@ -216,12 +217,12 @@
     ): Boolean {
         // Check if there's also an immersive change during this launch.
         val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask ->
-            findDesktopTaskChange(info, exitingTask)
+            findTaskChange(info, exitingTask)
         }
         val minimizeChange = pending.minimizingTask?.let { minimizingTask ->
-            findDesktopTaskChange(info, minimizingTask)
+            findTaskChange(info, minimizingTask)
         }
-        val launchChange = findDesktopTaskChange(info, pending.launchingTask)
+        val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask)
         if (launchChange == null) {
             check(minimizeChange == null)
             check(immersiveExitChange == null)
@@ -291,7 +292,7 @@
     ): Boolean {
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false
 
-        val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask)
+        val minimizeChange = findTaskChange(info, pending.minimizingTask)
         if (minimizeChange == null) {
             logW("Should have minimizing desktop task")
             return false
@@ -417,8 +418,24 @@
         }
     }
 
-    private fun findDesktopTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
-        return info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+    private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
+        info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+
+    private fun findDesktopTaskLaunchChange(
+        info: TransitionInfo,
+        launchTaskId: Int?
+    ): TransitionInfo.Change? {
+        return if (launchTaskId != null) {
+            // Launching a known task (probably from background or moving to front), so
+            // specifically look for it.
+            findTaskChange(info, launchTaskId)
+        } else {
+            // Launching a new task, so the first opening freeform task.
+            info.changes.firstOrNull { change ->
+                change.mode == TRANSIT_OPEN
+                        && change.taskInfo != null && change.taskInfo!!.isFreeform
+            }
+        }
     }
 
     private fun WindowContainerTransaction?.merge(
@@ -441,7 +458,7 @@
         /** A task is opening or moving to front. */
         data class Launch(
             override val transition: IBinder,
-            val launchingTask: Int,
+            val launchingTask: Int?,
             val minimizingTask: Int?,
             val exitingImmersiveTask: Int?,
         ) : PendingMixedTransition()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 08ca55f..7fcb767 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -411,6 +411,12 @@
             desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString())
         // Remove task from unminimized task if it is minimized.
         unminimizeTask(displayId, taskId)
+        // Mark task as not in immersive if it was immersive.
+        setTaskInFullImmersiveState(
+            displayId = displayId,
+            taskId = taskId,
+            immersive = false
+        )
         removeActiveTask(taskId)
         removeVisibleTask(taskId)
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
@@ -487,6 +493,9 @@
             mainCoroutineScope.launch {
                 try {
                     persistentRepository.addOrUpdateDesktop(
+                        // Use display id as desktop id for now since only once desktop per display
+                        // is supported.
+                        desktopId = displayId,
                         visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks,
                         minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks,
                         freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder
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 6928c25..4db0be5 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
@@ -89,7 +89,6 @@
 import com.android.wm.shell.common.SingleInstanceRemoteListener
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
-import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
 import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
@@ -618,25 +617,18 @@
     private fun moveBackgroundTaskToFront(taskId: Int, remoteTransition: RemoteTransition?) {
         logV("moveBackgroundTaskToFront taskId=%s", taskId)
         val wct = WindowContainerTransaction()
-        // TODO: b/342378842 - Instead of using default display, support multiple displays
-        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
-            wct = wct,
-            displayId = DEFAULT_DISPLAY,
-            excludeTaskId = taskId,
-        )
         wct.startTask(
             taskId,
             ActivityOptions.makeBasic().apply {
                 launchWindowingMode = WINDOWING_MODE_FREEFORM
             }.toBundle(),
         )
-        val transition = startLaunchTransition(
+        startLaunchTransition(
             TRANSIT_OPEN,
             wct,
             taskId,
             remoteTransition = remoteTransition
         )
-        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /**
@@ -655,47 +647,53 @@
         }
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
-        val result = desktopImmersiveController.exitImmersiveIfApplicable(
-            wct = wct,
-            displayId = taskInfo.displayId,
-            excludeTaskId = taskInfo.taskId,
-        )
-        val exitResult = if (result is ExitResult.Exit) { result } else { null }
-        val transition = startLaunchTransition(
+        startLaunchTransition(
             transitionType = TRANSIT_TO_FRONT,
             wct = wct,
-            taskId = taskInfo.taskId,
-            exitingImmersiveTask = exitResult?.exitingTask,
+            launchingTaskId = taskInfo.taskId,
             remoteTransition = remoteTransition,
             displayId = taskInfo.displayId,
         )
-        exitResult?.runOnTransitionStart?.invoke(transition)
     }
 
     private fun startLaunchTransition(
         transitionType: Int,
         wct: WindowContainerTransaction,
-        taskId: Int,
-        exitingImmersiveTask: Int? = null,
+        launchingTaskId: Int?,
         remoteTransition: RemoteTransition? = null,
         displayId: Int = DEFAULT_DISPLAY,
     ): IBinder {
-        val taskIdToMinimize = addAndGetMinimizeChanges(displayId, wct, taskId)
+        val taskIdToMinimize = if (launchingTaskId != null) {
+            addAndGetMinimizeChanges(displayId, wct, newTaskId = launchingTaskId)
+        } else {
+            logW("Starting desktop task launch without checking the task-limit")
+            // TODO(b/378920066): This currently does not respect the desktop window limit.
+            //  It's possible that |launchingTaskId| is null when launching using an intent, and
+            //  the task-limit should be respected then too.
+            null
+        }
+        val exitImmersiveResult = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = displayId,
+            excludeTaskId = launchingTaskId,
+        )
         if (remoteTransition == null) {
             val t = desktopMixedTransitionHandler.startLaunchTransition(
                 transitionType = transitionType,
                 wct = wct,
-                taskId = taskId,
+                taskId = launchingTaskId,
                 minimizingTaskId = taskIdToMinimize,
-                exitingImmersiveTask = exitingImmersiveTask,
+                exitingImmersiveTask = exitImmersiveResult.asExit()?.exitingTask,
             )
             taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) }
+            exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
             return t
         }
         if (taskIdToMinimize == null) {
             val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
             val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
             remoteTransitionHandler.setTransition(t)
+            exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
             return t
         }
         val remoteTransitionHandler =
@@ -704,6 +702,7 @@
         val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
         remoteTransitionHandler.setTransition(t)
         taskIdToMinimize.let { addPendingMinimizeTransition(t, it) }
+        exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
         return t
     }
 
@@ -1472,10 +1471,14 @@
                 )
             }
             WINDOWING_MODE_FREEFORM -> {
-                // TODO(b/336289597): This currently does not respect the desktop window limit.
                 val wct = WindowContainerTransaction()
                 wct.sendPendingIntent(launchIntent, fillIn, options.toBundle())
-                transitions.startTransition(TRANSIT_OPEN, wct, null)
+                startLaunchTransition(
+                    transitionType = TRANSIT_OPEN,
+                    wct = wct,
+                    launchingTaskId = null,
+                    displayId = callingTaskInfo.displayId
+                )
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 49cf8ae..35e6c8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -46,6 +46,8 @@
             "ShellBackPreview"),
     WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_TASK_OBSERVER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+            Consts.TAG_WM_SHELL),
     // TODO(b/282232877): turn logToLogcat to false.
     WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM_SHELL),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index b58f068..68dc0f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -45,9 +45,15 @@
      */
     void onRunningTaskChanged(in RunningTaskInfo taskInfo);
 
-    /** A task has moved to front. */
-    void onTaskMovedToFront(in GroupedTaskInfo[] visibleTasks);
+    /** A task has moved to front. Only used if enableShellTopTaskTracking() is disabled. */
+    void onTaskMovedToFront(in GroupedTaskInfo taskToFront);
 
-    /** A task info has changed. */
+    /** A task info has changed. Only used if enableShellTopTaskTracking() is disabled. */
     void onTaskInfoChanged(in RunningTaskInfo taskInfo);
+
+    /**
+     * If enableShellTopTaskTracking() is enabled, this reports the set of all visible tasks.
+     * Otherwise, this reports only the new top most visible task.
+     */
+    void onVisibleTasksChanged(in GroupedTaskInfo[] visibleTasks);
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 9911669..6da4f51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -17,14 +17,18 @@
 package com.android.wm.shell.recents;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.content.pm.PackageManager.FEATURE_PC;
 
+import static com.android.wm.shell.Flags.enableShellTopTaskTracking;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
 
 import android.Manifest;
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RecentTaskInfo;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
 import android.app.IApplicationThread;
 import android.app.KeyguardManager;
@@ -65,7 +69,6 @@
 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.Transitions;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -111,6 +114,11 @@
     private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>();
 
     /**
+     * Cached list of the visible tasks, sorted from top most to bottom most.
+     */
+    private final List<RunningTaskInfo> mVisibleTasks = new ArrayList<>();
+
+    /**
      * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
      * supported.
      */
@@ -170,10 +178,8 @@
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
         mDesktopRepository.ifPresent(it -> it.addActiveTaskListener(this));
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
-                    mMainExecutor);
-        }
+        mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
+                mMainExecutor);
         mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener(
                 mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged());
     }
@@ -205,7 +211,7 @@
         mTaskSplitBoundsMap.put(taskId1, splitBounds);
         mTaskSplitBoundsMap.put(taskId2, splitBounds);
         notifyRecentTasksChanged();
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Add split pair: %d, %d, %s",
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Add split pair: %d, %d, %s",
                 taskId1, taskId2, splitBounds);
         return true;
     }
@@ -221,7 +227,7 @@
             mTaskSplitBoundsMap.remove(taskId);
             mTaskSplitBoundsMap.remove(pairedTaskId);
             notifyRecentTasksChanged();
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Remove split pair: %d, %d",
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Remove split pair: %d, %d",
                     taskId, pairedTaskId);
         }
     }
@@ -234,7 +240,17 @@
 
         // We could do extra verification of requiring both taskIds of a pair and verifying that
         // the same split bounds object is returned... but meh. Seems unnecessary.
-        return mTaskSplitBoundsMap.get(taskId);
+        SplitBounds splitBounds = mTaskSplitBoundsMap.get(taskId);
+        if (splitBounds != null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                    "getSplitBoundsForTaskId: taskId=%d splitBoundsTasks=[%d, %d]", taskId,
+                    splitBounds.leftTopTaskId, splitBounds.rightBottomTaskId);
+        } else {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                    "getSplitBoundsForTaskId: expected split bounds for taskId=%d but not found",
+                    taskId);
+        }
+        return splitBounds;
     }
 
     @Override
@@ -249,7 +265,10 @@
 
     @Override
     public void onTaskStackChanged() {
-        notifyRecentTasksChanged();
+        if (!enableShellTopTaskTracking()) {
+            // Skip notifying recent tasks changed whenever task stack changes
+            notifyRecentTasksChanged();
+        }
     }
 
     @Override
@@ -263,15 +282,18 @@
         notifyRecentTasksChanged();
     }
 
-    public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskAdded(RunningTaskInfo taskInfo) {
         notifyRunningTaskAppeared(taskInfo);
     }
 
-    public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskRemoved(RunningTaskInfo taskInfo) {
         // Remove any split pairs associated with this task
         removeSplitPair(taskInfo.taskId);
-        notifyRecentTasksChanged();
         notifyRunningTaskVanished(taskInfo);
+        if (!enableShellTopTaskTracking()) {
+            // Only notify recent tasks changed if we aren't already notifying the visible tasks
+            notifyRecentTasksChanged();
+        }
     }
 
     /**
@@ -279,7 +301,7 @@
      *
      * This currently includes windowing mode and visibility.
      */
-    public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskRunningInfoChanged(RunningTaskInfo taskInfo) {
         notifyRecentTasksChanged();
         notifyRunningTaskChanged(taskInfo);
     }
@@ -290,14 +312,21 @@
     }
 
     @Override
+    public void onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo) {
+        notifyTaskMovedToFront(runningTaskInfo);
+    }
+
+    @Override
     public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
         notifyTaskInfoChanged(taskInfo);
     }
 
     @Override
-    public void onTaskMovedToFrontThroughTransition(
-            ActivityManager.RunningTaskInfo runningTaskInfo) {
-        notifyTaskMovedToFront(runningTaskInfo);
+    public void onVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
+        mVisibleTasks.clear();
+        mVisibleTasks.addAll(visibleTasks);
+        // Notify with all the info and not just the running task info
+        notifyVisibleTasksChanged(visibleTasks);
     }
 
     @VisibleForTesting
@@ -316,7 +345,7 @@
     /**
      * Notify the running task listener that a task appeared on desktop environment.
      */
-    private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
                 || taskInfo.realActivity == null) {
@@ -330,9 +359,25 @@
     }
 
     /**
+     * Notify the running task listener that a task was changed on desktop environment.
+     */
+    private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) {
+        if (mListener == null
+                || !shouldEnableRunningTasksForDesktopMode()
+                || taskInfo.realActivity == null) {
+            return;
+        }
+        try {
+            mListener.onRunningTaskChanged(taskInfo);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed call onRunningTaskChanged", e);
+        }
+    }
+
+    /**
      * Notify the running task listener that a task was removed on desktop environment.
      */
-    private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
                 || taskInfo.realActivity == null) {
@@ -346,25 +391,30 @@
     }
 
     /**
-     * Notify the running task listener that a task was changed on desktop environment.
+     * Notify the recents task listener that a task moved to front via a transition.
      */
-    private void notifyRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
         if (mListener == null
-                || !shouldEnableRunningTasksForDesktopMode()
-                || taskInfo.realActivity == null) {
+                || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
+                || taskInfo.realActivity == null
+                || enableShellTopTaskTracking()) {
             return;
         }
         try {
-            mListener.onRunningTaskChanged(taskInfo);
+            mListener.onTaskMovedToFront(GroupedTaskInfo.forFullscreenTasks(taskInfo));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed call onRunningTaskChanged", e);
+            Slog.w(TAG, "Failed call onTaskMovedToFront", e);
         }
     }
 
+    /**
+     * Notify the recents task listener that a task changed via a transition.
+     */
     private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
-                || taskInfo.realActivity == null) {
+                || taskInfo.realActivity == null
+                || enableShellTopTaskTracking()) {
             return;
         }
         try {
@@ -374,17 +424,21 @@
         }
     }
 
-    private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+    /**
+     * Notifies that the test of visible tasks have changed.
+     */
+    private void notifyVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
-                || taskInfo.realActivity == null) {
+                || !enableShellTopTaskTracking()) {
             return;
         }
         try {
-            GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo);
-            mListener.onTaskMovedToFront(new GroupedTaskInfo[]{ runningTask });
+            // Compute the visible recent tasks in order, and move the task to the top
+            mListener.onVisibleTasksChanged(generateList(visibleTasks)
+                    .toArray(new GroupedTaskInfo[0]));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed call onTaskMovedToFront", e);
+            Slog.w(TAG, "Failed call onVisibleTasksChanged", e);
         }
     }
 
@@ -397,6 +451,11 @@
     @VisibleForTesting
     void registerRecentTasksListener(IRecentTasksListener listener) {
         mListener = listener;
+        if (enableShellTopTaskTracking()) {
+            ProtoLog.v(WM_SHELL_TASK_OBSERVER, "registerRecentTasksListener");
+            // Post a notification for the current set of visible tasks
+            mMainExecutor.executeDelayed(() -> notifyVisibleTasksChanged(mVisibleTasks), 0);
+        }
     }
 
     @VisibleForTesting
@@ -411,14 +470,18 @@
 
     @VisibleForTesting
     ArrayList<GroupedTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
-        // Note: the returned task list is from the most-recent to least-recent order
-        final List<RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks(
-                maxNum, flags, userId);
+        // Note: the returned task list is ordered from the most-recent to least-recent order
+        return generateList(mActivityTaskManager.getRecentTasks(maxNum, flags, userId));
+    }
 
+    /**
+     * Generates a list of GroupedTaskInfos for the given list of tasks.
+     */
+    private <T extends TaskInfo> ArrayList<GroupedTaskInfo> generateList(@NonNull List<T> tasks) {
         // Make a mapping of task id -> task info
         final SparseArray<TaskInfo> rawMapping = new SparseArray<>();
-        for (int i = 0; i < rawList.size(); i++) {
-            final TaskInfo taskInfo = rawList.get(i);
+        for (int i = 0; i < tasks.size(); i++) {
+            final TaskInfo taskInfo = tasks.get(i);
             rawMapping.put(taskInfo.taskId, taskInfo);
         }
 
@@ -427,10 +490,10 @@
 
         int mostRecentFreeformTaskIndex = Integer.MAX_VALUE;
 
+        ArrayList<GroupedTaskInfo> groupedTasks = new ArrayList<>();
         // Pull out the pairs as we iterate back in the list
-        ArrayList<GroupedTaskInfo> recentTasks = new ArrayList<>();
-        for (int i = 0; i < rawList.size(); i++) {
-            final RecentTaskInfo taskInfo = rawList.get(i);
+        for (int i = 0; i < tasks.size(); i++) {
+            final TaskInfo taskInfo = tasks.get(i);
             if (!rawMapping.contains(taskInfo.taskId)) {
                 // If it's not in the mapping, then it was already paired with another task
                 continue;
@@ -441,7 +504,7 @@
                     && mDesktopRepository.get().isActiveTask(taskInfo.taskId)) {
                 // Freeform tasks will be added as a separate entry
                 if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
-                    mostRecentFreeformTaskIndex = recentTasks.size();
+                    mostRecentFreeformTaskIndex = groupedTasks.size();
                 }
                 // If task has their app bounds set to null which happens after reboot, set the
                 // app bounds to persisted lastFullscreenBounds. Also set the position in parent
@@ -461,36 +524,34 @@
             }
 
             final int pairedTaskId = mSplitTasks.get(taskInfo.taskId, INVALID_TASK_ID);
-            if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
-                    pairedTaskId)) {
+            if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) {
                 final TaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
                 rawMapping.remove(pairedTaskId);
-                recentTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
+                groupedTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
                         mTaskSplitBoundsMap.get(pairedTaskId)));
             } else {
-                recentTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
+                // TODO(346588978): Consolidate multiple visible fullscreen tasks into the same
+                //  grouped task
+                groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
             }
         }
 
         // Add a special entry for freeform tasks
         if (!freeformTasks.isEmpty()) {
-            recentTasks.add(mostRecentFreeformTaskIndex,
+            groupedTasks.add(mostRecentFreeformTaskIndex,
                     GroupedTaskInfo.forFreeformTasks(
                             freeformTasks,
                             minimizedFreeformTasks));
         }
 
-        return recentTasks;
-    }
+        if (enableShellTopTaskTracking()) {
+            // We don't current send pinned tasks as a part of recent or running tasks, so remove
+            // them from the list here
+            groupedTasks.removeIf(
+                    gti -> gti.getTaskInfo1().getWindowingMode() == WINDOWING_MODE_PINNED);
+        }
 
-    /**
-     * Returns the top running leaf task.
-     */
-    @Nullable
-    public ActivityManager.RunningTaskInfo getTopRunningTask() {
-        List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1,
-                false /* filterOnlyVisibleRecents */);
-        return tasks.isEmpty() ? null : tasks.get(0);
+        return groupedTasks;
     }
 
     /**
@@ -498,12 +559,13 @@
      * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task.
      */
     @Nullable
-    public ActivityManager.RunningTaskInfo getTopRunningTask(
+    public RunningTaskInfo getTopRunningTask(
             @Nullable WindowContainerToken ignoreTaskToken) {
-        List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2,
-                false /* filterOnlyVisibleRecents */);
+        final List<RunningTaskInfo> tasks = enableShellTopTaskTracking()
+                ? mVisibleTasks
+                : mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */);
         for (int i = 0; i < tasks.size(); i++) {
-            final ActivityManager.RunningTaskInfo task = tasks.get(i);
+            final RunningTaskInfo task = tasks.get(i);
             if (task.token.equals(ignoreTaskToken)) {
                 continue;
             }
@@ -541,7 +603,7 @@
     }
 
     /**
-     * Find the background task that match the given taskId.
+     * Find the background task (in the recent tasks list) that matches the given taskId.
      */
     @Nullable
     public RecentTaskInfo findTaskInBackground(int taskId) {
@@ -638,29 +700,34 @@
             }
 
             @Override
-            public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskAppeared(taskInfo));
             }
 
             @Override
-            public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskVanished(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskVanished(taskInfo));
             }
 
             @Override
-            public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskChanged(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskChanged(taskInfo));
             }
 
             @Override
-            public void onTaskMovedToFront(GroupedTaskInfo[] taskInfo) {
-                mListener.call(l -> l.onTaskMovedToFront(taskInfo));
+            public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
+                mListener.call(l -> l.onTaskMovedToFront(taskToFront));
             }
 
             @Override
             public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onTaskInfoChanged(taskInfo));
             }
+
+            @Override
+            public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+                mListener.call(l -> l.onVisibleTasksChanged(visibleTasks));
+            }
         };
 
         public IRecentTasksImpl(RecentTasksController controller) {
@@ -714,12 +781,12 @@
         }
 
         @Override
-        public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) {
-            final ActivityManager.RunningTaskInfo[][] tasks =
-                    new ActivityManager.RunningTaskInfo[][]{null};
+        public RunningTaskInfo[] getRunningTasks(int maxNum) {
+            final RunningTaskInfo[][] tasks =
+                    new RunningTaskInfo[][]{null};
             executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
                     (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
-                            .toArray(new ActivityManager.RunningTaskInfo[0]),
+                            .toArray(new RunningTaskInfo[0]),
                     true /* blocking */);
             return tasks[0];
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
index d28a462..93f2e4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -17,37 +17,162 @@
 package com.android.wm.shell.recents
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.os.IBinder
 import android.util.ArrayMap
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.window.DesktopModeFlags
 import android.window.TransitionInfo
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.Flags.enableShellTopTaskTracking
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER
 import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import dagger.Lazy
+import java.io.PrintWriter
+import java.util.StringJoiner
 import java.util.concurrent.Executor
 
 /**
- * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners
- * about task stack changes.
+ * A [Transitions.TransitionObserver] that observes shell transitions, tracks the visible tasks
+ * and notifies listeners whenever the visible tasks change (at the start and end of a transition).
  *
- * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it
+ * This can be replaced once we have a generalized task repository tracking visible tasks.
  */
 class TaskStackTransitionObserver(
+    shellInit: ShellInit,
+    private val shellTaskOrganizer: Lazy<ShellTaskOrganizer>,
+    private val shellCommandHandler: ShellCommandHandler,
     private val transitions: Lazy<Transitions>,
-    shellInit: ShellInit
-) : Transitions.TransitionObserver {
+) : Transitions.TransitionObserver, ShellTaskOrganizer.TaskVanishedListener {
+
+    // List of currently visible tasks sorted in z-order from top-most to bottom-most, only used
+    // when Flags.enableShellTopTaskTracking() is enabled.
+    private var visibleTasks: MutableList<RunningTaskInfo> = mutableListOf()
+    private val pendingCloseTasks: MutableList<RunningTaskInfo> = mutableListOf()
+    // Set of listeners to notify when the visible tasks change
     private val taskStackTransitionObserverListeners =
         ArrayMap<TaskStackTransitionObserverListener, Executor>()
+    // Used to filter out leaf-tasks
+    private val leafTaskFilter: TransitionUtil.LeafTaskFilter = TransitionUtil.LeafTaskFilter()
 
     init {
         shellInit.addInitCallback(::onInit, this)
     }
 
     fun onInit() {
+        shellTaskOrganizer.get().addTaskVanishedListener(this)
+        shellCommandHandler.addDumpCallback(::dump, this)
         transitions.get().registerObserver(this)
+
+        // TODO(346588978): We need to update the running tasks once the ShellTaskOrganizer is
+        // registered since there is no existing transition (yet) corresponding for the already
+        // visible tasks
+    }
+
+    /**
+     * This method handles transition ready when only
+     * DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL is set.
+     */
+    private fun onDesktopOnlyFlagTransitionReady(info: TransitionInfo) {
+        for (change in info.changes) {
+            if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
+                continue
+            }
+
+            val taskInfo = change.taskInfo
+            if (taskInfo == null || taskInfo.taskId == -1) {
+                continue
+            }
+
+            // Find the first task that is opening, this should be the one at the front after
+            // the transition
+            if (TransitionUtil.isOpeningType(change.mode)) {
+                notifyOnTaskMovedToFront(taskInfo)
+                break
+            } else if (change.mode == TRANSIT_CHANGE) {
+                notifyOnTaskChanged(taskInfo)
+            }
+        }
+    }
+
+    /**
+     * This method handles transition ready when Flags.enableShellTopTaskTracking() is set.
+     */
+    private fun onShellTopTaskTrackerFlagTransitionReady(info: TransitionInfo) {
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Transition ready: %d", info.debugId)
+
+        // Filter out non-leaf tasks (we will likely need them later, but visible task tracking
+        // is currently used only for visible leaf tasks)
+        val changesReversed = mutableListOf<TransitionInfo.Change>()
+        for (change in info.changes) {
+            if (!leafTaskFilter.test(change)) {
+                // Not a leaf task
+                continue
+            }
+            changesReversed.add(0, change)
+        }
+
+        // We iterate the change list in reverse order because changes are sorted top to bottom and
+        // we want to update the lists such that the top most tasks are inserted at the front last
+        var notifyChanges = false
+        for (change in changesReversed) {
+            val taskInfo = change.taskInfo
+            if (taskInfo == null || taskInfo.taskId == -1) {
+                // Not a valid task
+                continue
+            }
+
+            if (TransitionUtil.isClosingMode(change.mode)) {
+                ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tClosing task=%d", taskInfo.taskId)
+
+                // Closing task's visibilities are not committed until after the transition
+                // completes, so track such tasks so that we can notify on finish
+                if (!pendingCloseTasks.any { it.taskId == taskInfo.taskId }) {
+                    pendingCloseTasks.add(taskInfo)
+                }
+            } else if (TransitionUtil.isOpeningMode(change.mode)
+                    || TransitionUtil.isOrderOnly(change)) {
+                ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tOpening task=%d", taskInfo.taskId)
+
+                // Remove from pending close tasks list if it's being opened again
+                pendingCloseTasks.removeIf { it.taskId == taskInfo.taskId }
+                // Move the task to the front of the visible tasks list
+                visibleTasks.removeIf { it.taskId == taskInfo.taskId }
+                visibleTasks.add(0, taskInfo)
+                notifyChanges = true
+            }
+        }
+
+        // TODO(346588978): We should verify the task list has actually changed before notifying
+        //  (ie. starting an activity that's already top-most would result in no visible change)
+        if (notifyChanges) {
+            updateVisibleTasksList("transition-start")
+        }
+    }
+
+    private fun updateVisibleTasksList(reason: String) {
+        // This simply constructs a list of visible tasks, where the always-on-top tasks are moved
+        // to the front of the list in-order, to ensure that they match the visible z order
+        val orderedVisibleTasks = mutableListOf<RunningTaskInfo>()
+        var numAlwaysOnTop = 0
+        for (info in visibleTasks) {
+            if (info.windowingMode == WINDOWING_MODE_PINNED
+                    || info.configuration.windowConfiguration.isAlwaysOnTop) {
+                orderedVisibleTasks.add(numAlwaysOnTop, info)
+                numAlwaysOnTop++
+            } else {
+                orderedVisibleTasks.add(info)
+            }
+        }
+        visibleTasks = orderedVisibleTasks
+
+        dumpVisibleTasks(reason)
+        notifyVisibleTasksChanged(visibleTasks)
     }
 
     override fun onTransitionReady(
@@ -56,26 +181,10 @@
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction
     ) {
-        if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) {
-            for (change in info.changes) {
-                if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
-                    continue
-                }
-
-                val taskInfo = change.taskInfo
-                if (taskInfo == null || taskInfo.taskId == -1) {
-                    continue
-                }
-
-                // Find the first task that is opening, this should be the one at the front after
-                // the transition
-                if (TransitionUtil.isOpeningType(change.mode)) {
-                    notifyOnTaskMovedToFront(taskInfo)
-                    break
-                } else if (change.mode == TRANSIT_CHANGE) {
-                    notifyOnTaskChanged(taskInfo)
-                }
-            }
+        if (enableShellTopTaskTracking()) {
+            onShellTopTaskTrackerFlagTransitionReady(info)
+        } else if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) {
+            onDesktopOnlyFlagTransitionReady(info)
         }
     }
 
@@ -83,8 +192,35 @@
 
     override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
 
-    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+        if (enableShellTopTaskTracking()) {
+            if (pendingCloseTasks.isNotEmpty()) {
+                // Update the visible task list based on the pending close tasks
+                for (change in pendingCloseTasks) {
+                    visibleTasks.removeIf {
+                        it.taskId == change.taskId
+                    }
+                }
+                updateVisibleTasksList("transition-finished")
+            }
+        }
+    }
 
+    override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
+        if (!enableShellTopTaskTracking()) {
+            return
+        }
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Task vanished: task=%d", taskInfo?.taskId)
+        pendingCloseTasks.removeIf { it.taskId == taskInfo?.taskId }
+        if (visibleTasks.any { it.taskId == taskInfo?.taskId }) {
+            visibleTasks.removeIf { it.taskId == taskInfo?.taskId }
+            updateVisibleTasksList("task-vanished")
+        }
+    }
+
+    /**
+     * Adds a new task stack observer.
+     */
     fun addTaskStackTransitionObserverListener(
         taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
         executor: Executor
@@ -92,6 +228,9 @@
         taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
     }
 
+    /**
+     * Removes an existing task stack observer.
+     */
     fun removeTaskStackTransitionObserverListener(
         taskStackTransitionObserverListener: TaskStackTransitionObserverListener
     ) {
@@ -99,22 +238,66 @@
     }
 
     private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) {
+        if (enableShellTopTaskTracking()) {
+            return
+        }
         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
             executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
         }
     }
 
     private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) {
+        if (enableShellTopTaskTracking()) {
+            return
+        }
         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
             executor.execute { listener.onTaskChangedThroughTransition(taskInfo) }
         }
     }
 
+    private fun notifyVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {
+        taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+            executor.execute { listener.onVisibleTasksChanged(visibleTasks) }
+        }
+    }
+
+    fun dump(pw: PrintWriter, prefix: String) {
+        pw.println("${prefix}$TAG:")
+
+        if (visibleTasks.isEmpty()) {
+            pw.println("$prefix  visibleTasks=[]")
+        } else {
+            val stringJoiner = StringJoiner(",\n\t", "[\n\t", "\n]")
+            visibleTasks.forEach {
+                stringJoiner.add("id=${it.taskId} cmp=${it.baseIntent.component}")
+            }
+            pw.println("$prefix  visibleTasks=$stringJoiner")
+        }
+    }
+
+    /** Dumps the set of visible tasks to protolog */
+    private fun dumpVisibleTasks(reason: String) {
+        if (!WM_SHELL_TASK_OBSERVER.isEnabled) {
+            return
+        }
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tVisible tasks (%s)", reason)
+        for (task in visibleTasks) {
+            ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\t\ttaskId=%d package=%s", task.taskId,
+                task.baseIntent.component?.packageName)
+        }
+    }
+
     /** Listener to use to get updates regarding task stack from this observer */
     interface TaskStackTransitionObserverListener {
         /** Called when a task is moved to front. */
         fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+        /** Called when the set of visible tasks have changed. */
+        fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {}
         /** Called when a task info has changed. */
         fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {}
     }
+
+    companion object {
+        const val TAG = "TaskStackTransitionObserver"
+    }
 }
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 cc0e1df..7d1ffb8 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
@@ -1362,7 +1362,11 @@
     }
 
     void clearSplitPairedInRecents(@ExitReason int exitReason) {
-        if (!shouldBreakPairedTaskInRecents(exitReason) || !mShouldUpdateRecents) return;
+        if (!shouldBreakPairedTaskInRecents(exitReason) || !mShouldUpdateRecents) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearSplitPairedInRecents: skipping reason=%s",
+                    !mShouldUpdateRecents ? "shouldn't update" : exitReasonToString(exitReason));
+            return;
+        }
 
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearSplitPairedInRecents: reason=%s",
                 exitReasonToString(exitReason));
@@ -1608,6 +1612,8 @@
     private void updateRecentTasksSplitPair() {
         // Preventing from single task update while processing recents.
         if (!mShouldUpdateRecents || !mPausingTasks.isEmpty()) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateRecentTasksSplitPair: skipping reason=%s",
+                    !mShouldUpdateRecents ? "shouldn't update" : "no pausing tasks");
             return;
         }
         mRecentTasks.ifPresent(recentTasks -> {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt
new file mode 100644
index 0000000..e1120bd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+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.MAXIMIZE_APP
+import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Maximize app window by dragging it to the top drag zone.
+ *
+ * Assert that the app window keeps the same increases in size, filling the vertical and horizontal
+ * stable display bounds.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MaximizeAppWindowWithDragToTopDragZoneLandscape : MaximizeAppWindowWithDragToTopDragZone(
+    rotation = ROTATION_90
+) {
+    @ExpectedScenarios(["MAXIMIZE_APP"])
+    @Test
+    override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt
new file mode 100644
index 0000000..fb910c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.MAXIMIZE_APP
+import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Maximize app window by dragging it to the top drag zone.
+ *
+ * Assert that the app window keeps the same increases in size, filling the vertical and horizontal
+ * stable display bounds.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MaximizeAppWindowWithDragToTopDragZonePortrait : MaximizeAppWindowWithDragToTopDragZone() {
+    @ExpectedScenarios(["MAXIMIZE_APP"])
+    @Test
+    override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt
index 351a700..f9bf49e 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt
@@ -57,7 +57,7 @@
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         nonResizeableApp.launchViaIntent(wmHelper)
     }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt
index 3f9927f..16e5373 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt
@@ -40,7 +40,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         newTasksApp.launchViaIntent(wmHelper)
         imeApp.launchViaIntent(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt
index 6d52a11..c43a575 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt
@@ -47,7 +47,7 @@
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         // Set string extra to ensure the app is on PiP mode at launch
         pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true"))
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         newTasksApp.launchViaIntent(wmHelper)
         imeApp.launchViaIntent(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt
index 91cfd17..786a8b7 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt
@@ -36,7 +36,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
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 967bd29..0f546cd 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,7 @@
 
     @Test
     open fun enterDesktopWithDrag() {
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopModeWithDrag(wmHelper, device)
     }
 
     @After
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index f442fdb..2800839 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -46,7 +46,7 @@
             instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt
index 6637b01..5cf51e3 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt
@@ -66,7 +66,7 @@
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         testApp.cornerResize(
             wmHelper,
             device,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index a54d497..d2be494 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -58,7 +58,7 @@
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
index a2b88f2..60a0fb5 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
@@ -59,7 +59,7 @@
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
index b548363..971637b 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
@@ -61,7 +61,7 @@
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
-        testApp1.enterDesktopWithDrag(wmHelper, device)
+        testApp1.enterDesktopMode(wmHelper, device)
         testApp2.launchViaIntent(wmHelper)
         testApp3.launchViaIntent(wmHelper)
     }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt
index b86765e..7987f7e 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt
@@ -58,7 +58,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         newTasksApp.launchViaIntent(wmHelper)
         imeApp.launchViaIntent(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt
index aad266f..6ce36f5 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt
@@ -61,7 +61,7 @@
         tapl.setExpectedRotation(rotation.value)
         tapl.enableTransientTaskbar(false)
         ChangeDisplayOrientationRule.setRotation(rotation)
-        firstApp.enterDesktopWithDrag(wmHelper, device)
+        firstApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
index bfee318..eefa0bb 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
@@ -61,7 +61,7 @@
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         newTasksApp.launchViaIntent(wmHelper)
         imeApp.launchViaIntent(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt
index 5b1b64e..0226eb3 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt
@@ -65,7 +65,7 @@
         tapl.setExpectedRotation(rotation.value)
         // Set string extra to ensure the app is on PiP mode at launch
         pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true"))
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
         mailApp.launchViaIntent(wmHelper)
         newTasksApp.launchViaIntent(wmHelper)
         imeApp.launchViaIntent(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
index a7cebf4..6463623 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
@@ -67,7 +67,7 @@
         tapl.setEnableRotation(true)
         ChangeDisplayOrientationRule.setRotation(rotation)
         tapl.setExpectedRotation(rotation.value)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt
index 6780238..f198cfe 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt
@@ -60,7 +60,7 @@
         )
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
index 2b40497..fd4c243 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
@@ -56,7 +56,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
index b4bd7e1..62e860e 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
@@ -56,7 +56,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt
index f08e50e..de330e0 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt
@@ -59,7 +59,7 @@
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(0)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt
index ce235d4..4b3f15f 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt
@@ -67,7 +67,7 @@
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(0)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt
index 0051952..a108367 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt
@@ -62,7 +62,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt
index dad2eb6..1455bd1 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt
@@ -54,7 +54,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        testApp.enterDesktopWithDrag(wmHelper, device)
+        testApp.enterDesktopMode(wmHelper, device)
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt
new file mode 100644
index 0000000..528ca7e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell
+
+import com.android.wm.shell.common.ShellExecutor
+
+/**
+ * Test ShellExecutor that runs everything synchronously.
+ */
+class TestSyncExecutor : ShellExecutor {
+    override fun execute(runnable: Runnable) {
+        runnable.run()
+    }
+
+    override fun executeDelayed(runnable: Runnable, delayMillis: Long) {
+        runnable.run()
+    }
+
+    override fun removeCallbacks(runnable: Runnable) {
+    }
+
+    override fun hasCallback(runnable: Runnable): Boolean {
+        return false
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index f21f264..62717a3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -31,6 +31,8 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TransitionType
@@ -47,6 +49,7 @@
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.StubTransaction
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -491,6 +494,72 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_unknownLaunchingTask_animates() {
+        val wct = WindowContainerTransaction()
+        val task = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+        whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any()))
+            .thenReturn(mock())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = null,
+        )
+
+        val started = mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(createChange(task, mode = TRANSIT_OPEN))
+            ),
+            StubTransaction(),
+            StubTransaction(),
+        ) { }
+
+        assertThat(started).isEqualTo(true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_unknownLaunchingTaskOverImmersive_animatesImmersiveChange() {
+        val wct = WindowContainerTransaction()
+        val immersiveTask = createTask(WINDOWING_MODE_FREEFORM)
+        val openingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+        whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any()))
+            .thenReturn(mock())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = null,
+            exitingImmersiveTask = immersiveTask.taskId,
+        )
+
+        val immersiveChange = createChange(immersiveTask, mode = TRANSIT_CHANGE)
+        val openingChange = createChange(openingTask, mode = TRANSIT_OPEN)
+        val started = mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(immersiveChange, openingChange)
+            ),
+            StubTransaction(),
+            StubTransaction(),
+        ) { }
+
+        assertThat(started).isEqualTo(true)
+        verify(desktopImmersiveController)
+            .animateResizeChange(eq(immersiveChange), any(), any(), any())
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
     fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() {
         val wct = WindowContainerTransaction()
@@ -712,9 +781,13 @@
         changes.forEach { change -> addChange(change) }
     }
 
-    private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
+    private fun createChange(
+        task: RunningTaskInfo,
+        @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE
+    ): TransitionInfo.Change =
         TransitionInfo.Change(task.token, SurfaceControl()).apply {
             taskInfo = task
+            setMode(mode)
         }
 
     private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
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 ad266ea..2319716 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
@@ -100,6 +100,7 @@
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener
@@ -167,6 +168,7 @@
 import org.mockito.Mockito.times
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
@@ -294,10 +296,10 @@
     whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>()))
-      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
+      .thenReturn(ExitResult.NoExit)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
-      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
+      .thenReturn(ExitResult.NoExit)
 
     controller = createController()
     controller.setSplitScreenController(splitScreenController)
@@ -1833,7 +1835,8 @@
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = task.taskId,
         runOnTransitionStart = runOnTransit,
       ))
@@ -3214,13 +3217,43 @@
   fun newWindow_fromFreeformAddsNewWindow() {
     setUpLandscapeDisplay()
     val task = setUpFreeformTask()
-    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    val wctCaptor = argumentCaptor<WindowContainerTransaction>()
+    val transition = Binder()
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .thenReturn(ExitResult.NoExit)
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
+      .thenReturn(transition)
+
     runOpenNewWindow(task)
-    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
-    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+
+    verify(desktopMixedTransitionHandler)
+      .startLaunchTransition(anyInt(), wctCaptor.capture(), anyOrNull(), anyOrNull(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions)
       .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
   }
 
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFreeform_exitsImmersiveIfNeeded() {
+    setUpLandscapeDisplay()
+    val immersiveTask = setUpFreeformTask()
+    val task = setUpFreeformTask()
+    val runOnStart = RunOnStartTransitionCallback()
+    val transition = Binder()
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart))
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
+      .thenReturn(transition)
+
+    runOpenNewWindow(task)
+
+    runOnStart.assertOnlyInvocation(transition)
+  }
+
   private fun runOpenNewWindow(task: RunningTaskInfo) {
     markTaskVisible(task)
     task.baseActivity = mock(ComponentName::class.java)
@@ -3314,7 +3347,8 @@
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = immersiveTask.taskId,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3719,7 +3753,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3740,7 +3775,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3760,7 +3796,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3782,7 +3819,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index 2b30bc3..fd3adab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -176,6 +176,30 @@
         assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
     }
 
+    @Test
+    fun testGetTaskById_singleTasks() {
+        val task1 = createTaskInfo(id = 1234)
+
+        val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1)
+
+        assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1)
+        assertThat(taskInfo.containsTask(1234)).isTrue()
+    }
+
+    @Test
+    fun testGetTaskById_multipleTasks() {
+        val task1 = createTaskInfo(id = 1)
+        val task2 = createTaskInfo(id = 2)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
+
+        val taskInfo = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
+
+        assertThat(taskInfo.getTaskById(1)).isEqualTo(task1)
+        assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
+        assertThat(taskInfo.containsTask(1)).isTrue()
+        assertThat(taskInfo.containsTask(2)).isTrue()
+    }
+
     private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
         taskId = id
         token = WindowContainerToken(mock(IWindowContainerToken::class.java))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index dede583..12c3978 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -610,7 +610,7 @@
         mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
 
         GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo);
-        verify(mRecentTasksListener).onTaskMovedToFront(eq(new GroupedTaskInfo[] { runningTask }));
+        verify(mRecentTasksListener).onTaskMovedToFront(eq(runningTask));
     }
 
     @Test
@@ -656,6 +656,35 @@
         assertEquals(splitBounds4, pair2Bounds);
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onTaskStackChanged_expectNoRecentsChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        mRecentTasksControllerReal.onTaskStackChanged();
+        verify(mRecentTasksListener, never()).onRecentTasksChanged();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onTaskRemoved_expectNoRecentsChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.onTaskRemoved(taskInfo);
+        verify(mRecentTasksListener, never()).onRecentTasksChanged();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onVisibleTasksChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.onVisibleTasksChanged(List.of(taskInfo));
+        verify(mRecentTasksListener, never()).onVisibleTasksChanged(any());
+    }
+
     /**
      * Helper to create a task with a given task id.
      */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
index efe4fb1..9919462 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -16,21 +16,36 @@
 
 package com.android.wm.shell.recents
 
-import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.TaskInfo
 import android.app.WindowConfiguration
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+import android.content.ComponentName
+import android.content.Intent
 import android.os.IBinder
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_FIRST_CUSTOM
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IWindowContainerToken
 import android.window.TransitionInfo
+import android.window.TransitionInfo.FLAG_MOVED_TO_TOP
 import android.window.WindowContainerToken
 import androidx.test.filters.SmallTest
 import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.TestSyncExecutor
 import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
@@ -61,7 +76,10 @@
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
     @Mock private lateinit var shellInit: ShellInit
-    @Mock lateinit var testExecutor: ShellExecutor
+    @Mock private lateinit var shellTaskOrganizerLazy: Lazy<ShellTaskOrganizer>
+    @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer
+    @Mock private lateinit var shellCommandHandler: ShellCommandHandler
+    @Mock private lateinit var testExecutor: ShellExecutor
     @Mock private lateinit var transitionsLazy: Lazy<Transitions>
     @Mock private lateinit var transitions: Transitions
     @Mock private lateinit var mockTransitionBinder: IBinder
@@ -73,24 +91,23 @@
         MockitoAnnotations.initMocks(this)
         shellInit = Mockito.spy(ShellInit(testExecutor))
         whenever(transitionsLazy.get()).thenReturn(transitions)
-        transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit)
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-            verify(shellInit)
-                .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
-            initRunnableCaptor.value.run()
-        } else {
-            transitionObserver.onInit()
-        }
+        whenever(shellTaskOrganizerLazy.get()).thenReturn(shellTaskOrganizer)
+        transitionObserver = TaskStackTransitionObserver(shellInit, shellTaskOrganizerLazy,
+            shellCommandHandler, transitionsLazy)
+
+        val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(shellInit)
+            .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+        initRunnableCaptor.value.run()
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun testRegistersObserverAtInit() {
         verify(transitions).registerObserver(same(transitionObserver))
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_freeformWindow_listenerNotified() {
         val listener = TestListener()
@@ -98,11 +115,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         callOnTransitionReady(transitionInfo)
         callOnTransitionFinished()
@@ -114,6 +131,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_fullscreenWindow_listenerNotified() {
         val listener = TestListener()
@@ -121,11 +139,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
-                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                TRANSIT_OPEN,
+                createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         callOnTransitionReady(transitionInfo)
         callOnTransitionFinished()
@@ -133,10 +151,11 @@
 
         assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1)
         assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
-            .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() {
         val listener = TestListener()
@@ -144,7 +163,7 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val freeformOpenChange =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val freeformReorderChange =
@@ -153,7 +172,7 @@
                 createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0)
+            TransitionInfoBuilder(TRANSIT_OPEN, 0)
                 .addChange(freeformOpenChange)
                 .addChange(freeformReorderChange)
                 .build()
@@ -169,6 +188,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun transitionMerged_withChange_onlyOpenChangeIsNotified() {
         val listener = TestListener()
@@ -178,11 +198,11 @@
         // Create open transition
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         // create change transition to be merged to above transition
         val mergedChange =
@@ -212,6 +232,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun transitionMerged_withOpen_lastOpenChangeIsNotified() {
         val listener = TestListener()
@@ -221,20 +242,20 @@
         // Create open transition
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         // create change transition to be merged to above transition
         val mergedChange =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val mergedTransitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(mergedChange).build()
         val mergedTransition = Mockito.mock(IBinder::class.java)
 
         callOnTransitionReady(transitionInfo)
@@ -250,6 +271,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() {
         val listener = TestListener()
@@ -257,11 +279,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val freeformState =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfoOpen =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(freeformState).build()
         callOnTransitionReady(transitionInfoOpen)
         callOnTransitionFinished()
         executor.flushAll()
@@ -276,7 +298,7 @@
         val fullscreenState =
             createChange(
                 WindowManager.TRANSIT_CHANGE,
-                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
         val transitionInfoChange =
             TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0)
@@ -301,6 +323,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun singleTransition_withOpenAndChange_onlyOpenIsNotified() {
         val listener = TestListener()
@@ -310,13 +333,13 @@
         // Creating multiple changes to be fired in a single transition
         val freeformState =
             createChange(
-                mode = WindowManager.TRANSIT_OPEN,
+                mode = TRANSIT_OPEN,
                 taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val fullscreenState =
             createChange(
                 mode = WindowManager.TRANSIT_CHANGE,
-                taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                taskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
 
         val transitionInfoWithChanges =
@@ -336,6 +359,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() {
         val listener = TestListener()
@@ -349,7 +373,7 @@
             listOf(
                     WindowConfiguration.WINDOWING_MODE_FREEFORM,
                     WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION,
-                    WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+                    WINDOWING_MODE_FULLSCREEN
                 )
                 .map { change ->
                     createChange(
@@ -376,19 +400,259 @@
         }
     }
 
-    class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
-        var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo()
-        var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>()
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun openTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
 
-        override fun onTaskMovedToFrontThroughTransition(
-            taskInfo: ActivityManager.RunningTaskInfo
-        ) {
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model opening another task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                    createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list from top to bottom is valid (opening, closing)
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(2, 1))
+
+        callOnTransitionFinished()
+        // Assert that after the transition finishes, there is only the opening task remaining
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3)
+        assertVisibleTasks(listener, listOf(2))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun toFrontTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model opening another task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the visible list from top to bottom is valid
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(2, 1))
+
+        // Model the first task moving to front
+        val toFrontTransition =
+            createTransitionInfo(TRANSIT_TO_FRONT,
+                listOf(
+                    createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN,
+                        FLAG_MOVED_TO_TOP),
+                )
+            )
+
+        callOnTransitionReady(toFrontTransition)
+        callOnTransitionFinished()
+        // Assert that the visible list from top to bottom is valid
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3)
+        assertVisibleTasks(listener, listOf(1, 2))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun closeTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        // Model a closing task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list hasn't changed (the close is pending)
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        callOnTransitionFinished()
+        // Assert that after the transition finishes, there is only the opening task remaining
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun changeTransition_visibleTasksUnchanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        // Model a closing task
+        val nextOpeningTransition =
+            createTransitionInfo(
+                TRANSIT_FIRST_CUSTOM,
+                listOf(
+                    createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list hasn't changed
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun taskVanished_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Trigger task vanished
+        val removedTaskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
+        transitionObserver.onTaskVanished(removedTaskInfo)
+
+        // Assert that the visible list is now empty
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun alwaysOnTop_taskIsTopMostVisible() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening PIP task
+        val pipOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_PINNED),
+                )
+            )
+
+        callOnTransitionReady(pipOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model an opening fullscreen task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(1, 2))
+    }
+
+    class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
+        // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is disabled
+        var taskInfoOnTaskMovedToFront = RunningTaskInfo()
+        var taskInfoOnTaskChanged = mutableListOf<RunningTaskInfo>()
+        // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is enabled
+        var visibleTasks = mutableListOf<TaskInfo>()
+        var visibleTasksUpdatedCount = 0
+
+        override fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {
             taskInfoOnTaskMovedToFront = taskInfo
         }
 
-        override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) {
+        override fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {
             taskInfoOnTaskChanged += taskInfo
         }
+
+        override fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {
+            this.visibleTasks.clear()
+            this.visibleTasks.addAll(visibleTasks)
+            visibleTasksUpdatedCount++
+        }
     }
 
     /** Simulate calling the onTransitionReady() method */
@@ -412,27 +676,64 @@
         transitionObserver.onTransitionMerged(merged, playing)
     }
 
+    /**
+     * Asserts that the listener has the given expected task ids (in order).
+     */
+    private fun assertVisibleTasks(
+        listener: TestListener,
+        expectedVisibleTaskIds: List<Int>
+    ) {
+        assertThat(listener.visibleTasks.size).isEqualTo(expectedVisibleTaskIds.size)
+        expectedVisibleTaskIds.forEachIndexed { index, taskId ->
+            assertThat(listener.visibleTasks[index].taskId).isEqualTo(taskId)
+        }
+    }
+
     companion object {
-        fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
-            val taskInfo = ActivityManager.RunningTaskInfo()
+        fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo {
+            val taskInfo = RunningTaskInfo()
+            taskInfo.baseIntent = Intent().setComponent(
+                ComponentName(javaClass.packageName, "Test"))
             taskInfo.taskId = taskId
             taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
-
+            if (windowingMode == WINDOWING_MODE_PINNED) {
+                taskInfo.configuration.windowConfiguration.isAlwaysOnTop = true
+            }
             return taskInfo
         }
 
         fun createChange(
             mode: Int,
-            taskInfo: ActivityManager.RunningTaskInfo
+            taskInfo: RunningTaskInfo,
+            flags: Int = 0,
         ): TransitionInfo.Change {
             val change =
                 TransitionInfo.Change(
                     WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)),
                     Mockito.mock(SurfaceControl::class.java)
                 )
+            change.flags = flags
             change.mode = mode
             change.taskInfo = taskInfo
             return change
         }
+
+        fun createChange(
+            mode: Int,
+            taskId: Int,
+            windowingMode: Int,
+            flags: Int = 0,
+        ): TransitionInfo.Change {
+            return createChange(mode, createTaskInfo(taskId, windowingMode), flags)
+        }
+
+        fun createTransitionInfo(
+            transitionType: Int,
+            changes: List<TransitionInfo.Change>
+        ): TransitionInfo {
+            return TransitionInfoBuilder(transitionType, 0)
+                .apply { changes.forEach { c -> this@apply.addChange(c) } }
+                .build()
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
index c36b88e..71af97e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
@@ -43,6 +43,7 @@
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
+import com.android.wm.shell.TestSyncExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.sysui.ShellInit;
@@ -475,27 +476,6 @@
         }
     }
 
-    private static class TestSyncExecutor implements ShellExecutor {
-        @Override
-        public void execute(Runnable runnable) {
-            runnable.run();
-        }
-
-        @Override
-        public void executeDelayed(Runnable runnable, long delayMillis) {
-            runnable.run();
-        }
-
-        @Override
-        public void removeCallbacks(Runnable runnable) {
-        }
-
-        @Override
-        public boolean hasCallback(Runnable runnable) {
-            return false;
-        }
-    }
-
     private TransitionInfo createUnfoldTransitionInfo() {
         TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0);
         TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class));
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index fcb7efc..e2db2c9 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -355,6 +355,7 @@
         "jni/AnimatedImageDrawable.cpp",
         "jni/Bitmap.cpp",
         "jni/BitmapRegionDecoder.cpp",
+        "jni/RuntimeXfermode.cpp",
         "jni/BufferUtils.cpp",
         "jni/HardwareBufferHelpers.cpp",
         "jni/BitmapFactory.cpp",
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index 15b2bac..56de568 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -28,6 +28,7 @@
 extern int register_android_graphics_Bitmap(JNIEnv*);
 extern int register_android_graphics_BitmapFactory(JNIEnv*);
 extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*);
+extern int register_android_graphics_RuntimeXfermode(JNIEnv*);
 extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env);
 extern int register_android_graphics_Camera(JNIEnv* env);
 extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env);
@@ -107,6 +108,7 @@
             REG_JNI(register_android_graphics_Bitmap),
             REG_JNI(register_android_graphics_BitmapFactory),
             REG_JNI(register_android_graphics_BitmapRegionDecoder),
+            REG_JNI(register_android_graphics_RuntimeXfermode),
             REG_JNI(register_android_graphics_ByteBufferStreamAdaptor),
             REG_JNI(register_android_graphics_Camera),
             REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index da23792..a7d855d 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -906,6 +906,13 @@
         paint->setBlendMode(mode);
     }
 
+    static void setRuntimeXfermode(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle,
+                                   jlong xfermodeHandle) {
+        Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+        SkBlender* blender = reinterpret_cast<SkBlender*>(xfermodeHandle);
+        paint->setBlender(sk_ref_sp(blender));
+    }
+
     static jlong setPathEffect(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong effectHandle) {
         Paint* obj = reinterpret_cast<Paint*>(objHandle);
         SkPathEffect* effect  = reinterpret_cast<SkPathEffect*>(effectHandle);
@@ -1233,6 +1240,7 @@
         {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader},
         {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter},
         {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode},
+        {"nSetXfermode", "(JJ)V", (void*)PaintGlue::setRuntimeXfermode},
         {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect},
         {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter},
         {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface},
diff --git a/libs/hwui/jni/RuntimeXfermode.cpp b/libs/hwui/jni/RuntimeXfermode.cpp
new file mode 100644
index 0000000..c1c8964
--- /dev/null
+++ b/libs/hwui/jni/RuntimeXfermode.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "GraphicsJNI.h"
+#include "RuntimeEffectUtils.h"
+#include "SkBlender.h"
+
+using namespace android::uirenderer;
+
+static void SkRuntimeEffectBuilder_delete(SkRuntimeEffectBuilder* builder) {
+    delete builder;
+}
+
+static jlong RuntimeXfermode_getNativeFinalizer(JNIEnv*, jobject) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeEffectBuilder_delete));
+}
+
+static jlong RuntimeXfermode_createBuilder(JNIEnv* env, jobject, jstring sksl) {
+    ScopedUtfChars strSksl(env, sksl);
+    auto result =
+            SkRuntimeEffect::MakeForBlender(SkString(strSksl.c_str()), SkRuntimeEffect::Options{});
+    if (result.effect.get() == nullptr) {
+        doThrowIAE(env, result.errorText.c_str());
+        return 0;
+    }
+    return reinterpret_cast<jlong>(new SkRuntimeEffectBuilder(std::move(result.effect)));
+}
+
+static jlong RuntimeXfermode_create(JNIEnv* env, jobject, jlong builderPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    sk_sp<SkBlender> blender = builder->makeBlender();
+    if (!blender) {
+        doThrowIAE(env);
+    }
+    return reinterpret_cast<jlong>(blender.release());
+}
+
+static void RuntimeXfermode_updateFloatArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                     jstring uniformName, jfloatArray uniforms,
+                                                     jboolean isColor) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess);
+    UpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), isColor);
+}
+
+static void RuntimeXfermode_updateFloatUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                jstring uniformName, jfloat value1, jfloat value2,
+                                                jfloat value3, jfloat value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const float values[4] = {value1, value2, value3, value4};
+    UpdateFloatUniforms(env, builder, name.c_str(), values, count, false);
+}
+
+static void RuntimeXfermode_updateIntArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                   jstring uniformName, jintArray uniforms) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaIntArray autoValues(env, uniforms, 0);
+    UpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length());
+}
+
+static void RuntimeXfermode_updateIntUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                              jstring uniformName, jint value1, jint value2,
+                                              jint value3, jint value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const int values[4] = {value1, value2, value3, value4};
+    UpdateIntUniforms(env, builder, name.c_str(), values, count);
+}
+
+static void RuntimeXfermode_updateChild(JNIEnv* env, jobject, jlong builderPtr, jstring childName,
+                                        jlong childPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, childName);
+    auto* child = reinterpret_cast<SkFlattenable*>(childPtr);
+    if (child) {
+        UpdateChild(env, builder, name.c_str(), child);
+    }
+}
+
+static const JNINativeMethod gRuntimeXfermodeMethods[] = {
+        {"nativeGetFinalizer", "()J", (void*)RuntimeXfermode_getNativeFinalizer},
+        {"nativeCreateBlenderBuilder", "(Ljava/lang/String;)J",
+         (void*)RuntimeXfermode_createBuilder},
+        {"nativeCreateNativeInstance", "(J)J", (void*)RuntimeXfermode_create},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
+         (void*)RuntimeXfermode_updateFloatArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V",
+         (void*)RuntimeXfermode_updateFloatUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V",
+         (void*)RuntimeXfermode_updateIntArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V",
+         (void*)RuntimeXfermode_updateIntUniforms},
+        {"nativeUpdateChild", "(JLjava/lang/String;J)V", (void*)RuntimeXfermode_updateChild},
+};
+
+int register_android_graphics_RuntimeXfermode(JNIEnv* env) {
+    android::RegisterMethodsOrDie(env, "android/graphics/RuntimeXfermode", gRuntimeXfermodeMethods,
+                                  NELEM(gRuntimeXfermodeMethods));
+
+    return 0;
+}
diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java
index 3cd5f52..da50f2c 100644
--- a/media/java/android/media/AudioPlaybackConfiguration.java
+++ b/media/java/android/media/AudioPlaybackConfiguration.java
@@ -19,6 +19,7 @@
 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL;
 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE;
 import static android.media.audio.Flags.FLAG_MUTED_BY_PORT_VOLUME_API;
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -39,6 +40,8 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -461,8 +464,12 @@
 
     /**
      * Returns information about the {@link AudioDeviceInfo} used for this playback.
-     * @return the audio playback device or null if the device is not available at the time of query
+     * @return the audio playback device or null if the device is not available at the time of
+     * query.
+     * @deprecated this information was never populated
      */
+    @Deprecated
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
     public @Nullable AudioDeviceInfo getAudioDeviceInfo() {
         final int deviceId;
         synchronized (mUpdateablePropLock) {
@@ -476,6 +483,23 @@
 
     /**
      * @hide
+     * Returns information about the List of {@link AudioDeviceInfo} used for this playback.
+     * @return the audio playback devices
+     */
+    @SystemApi
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public @NonNull List<AudioDeviceInfo> getAudioDeviceInfos() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getAudioDeviceInfo();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return audioDeviceInfos;
+    }
+
+    /**
+     * @hide
      * Return the audio session ID associated with this player.
      * See {@link AudioManager#generateAudioSessionId()}.
      * @return an audio session ID
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 80e5719..9394941 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -20,8 +20,10 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -1920,6 +1922,23 @@
     }
 
     /**
+     * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this
+     * AudioRecord.
+     * Note: The query is only valid if the AudioRecord is currently playing. If it is not,
+     * <code>getRoutedDevices()</code> will return an empty list.
+     */
+    @Override
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    public @NonNull List<AudioDeviceInfo> getRoutedDevices() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getRoutedDevice();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return audioDeviceInfos;
+    }
+
+    /**
      * Must match the native definition in frameworks/av/service/audioflinger/Audioflinger.h.
      */
     private static final long MAX_SHARED_AUDIO_HISTORY_MS = 5000;
diff --git a/media/java/android/media/AudioRouting.java b/media/java/android/media/AudioRouting.java
index 26fa631..22aa9a0 100644
--- a/media/java/android/media/AudioRouting.java
+++ b/media/java/android/media/AudioRouting.java
@@ -16,9 +16,16 @@
 
 package android.media;
 
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.os.Handler;
 import android.os.Looper;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * AudioRouting defines an interface for controlling routing and routing notifications in
  * AudioTrack and AudioRecord objects.
@@ -49,6 +56,22 @@
     public AudioDeviceInfo getRoutedDevice();
 
     /**
+     * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this
+     * AudioTrack/AudioRecord.
+     * Note: The query is only valid if the AudioTrack/AudioRecord is currently playing.
+     * If it is not, <code>getRoutedDevices()</code> will return an empty List.
+     */
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    default @NonNull List<AudioDeviceInfo> getRoutedDevices() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getRoutedDevice();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return new ArrayList<AudioDeviceInfo>();
+    }
+
+    /**
      * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
      * changes on this AudioTrack/AudioRecord.
      * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 03cd535..93a1831 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -17,8 +17,10 @@
 package android.media;
 
 import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -54,7 +56,9 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.NioUtils;
+import java.util.ArrayList;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -3783,6 +3787,8 @@
      * Returns an {@link AudioDeviceInfo} identifying the current routing of this AudioTrack.
      * Note: The query is only valid if the AudioTrack is currently playing. If it is not,
      * <code>getRoutedDevice()</code> will return null.
+     * Audio may play on multiple devices simultaneously (e.g. an alarm playing on headphones and
+     * speaker on a phone), so prefer using {@link #getRoutedDevices}.
      */
     @Override
     public AudioDeviceInfo getRoutedDevice() {
@@ -3793,6 +3799,23 @@
         return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS);
     }
 
+    /**
+     * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this
+     * AudioTrack.
+     * Note: The query is only valid if the AudioTrack is currently playing. If it is not,
+     * <code>getRoutedDevices()</code> will return an empty list.
+     */
+    @Override
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    public @NonNull List<AudioDeviceInfo> getRoutedDevices() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getRoutedDevice();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return audioDeviceInfos;
+    }
+
     private void tryToDisableNativeRoutingCallback() {
         synchronized (mRoutingChangeListeners) {
             if (mEnableSelfRoutingMonitor) {
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 3f9126a..1ecba31 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -16,10 +16,9 @@
 
 package android.media;
 
+import static android.media.tv.flags.Flags.FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY;
 import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN;
 
-import static com.android.media.flags.Flags.FLAG_UPDATE_CLIENT_PROFILE_PRIORITY;
-
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -996,7 +995,7 @@
      * @param niceValue the nice value.
      * @hide
      */
-    @FlaggedApi(FLAG_UPDATE_CLIENT_PROFILE_PRIORITY)
+    @FlaggedApi(FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY)
     @SystemApi
     @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public boolean updateResourcePriority(int priority, int niceValue) {
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index a0f8ae5..158bc7f 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -18,7 +18,9 @@
 
 import static android.Manifest.permission.BIND_IMS_SERVICE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -84,6 +86,7 @@
 import java.net.InetSocketAddress;
 import java.net.URL;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.HashMap;
@@ -1542,6 +1545,8 @@
      * Note: The query is only valid if the MediaPlayer is currently playing.
      * If the player is not playing, the returned device can be null or correspond to previously
      * selected device when the player was last active.
+     * Audio may play on multiple devices simultaneously (e.g. an alarm playing on headphones and
+     * speaker on a phone), so prefer using {@link #getRoutedDevices}.
      */
     @Override
     public AudioDeviceInfo getRoutedDevice() {
@@ -1552,6 +1557,23 @@
         return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS);
     }
 
+    /**
+     * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this
+     * MediaPlayer.
+     * Note: The query is only valid if the MediaPlayer is currently playing.
+     * If the player is not playing, the returned devices can be empty or correspond to previously
+     * selected devices when the player was last active.
+     */
+    @Override
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    public @NonNull List<AudioDeviceInfo> getRoutedDevices() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getRoutedDevice();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return audioDeviceInfos;
+    }
 
     /**
      * Sends device list change notification to all listeners.
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index 2d17bf5..f75bcf3 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -16,7 +16,10 @@
 
 package android.media;
 
+import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS;
+
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -1695,6 +1698,24 @@
         return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_INPUTS);
     }
 
+    /**
+     * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this
+     * MediaRecorder.
+     * Note: The query is only valid if the MediaRecorder is currently recording.
+     * If the recorder is not recording, the returned devices can be empty or correspond to
+     * previously selected devices when the recorder was last active.
+     */
+    @Override
+    @FlaggedApi(FLAG_ROUTED_DEVICE_IDS)
+    public @NonNull List<AudioDeviceInfo> getRoutedDevices() {
+        List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>();
+        AudioDeviceInfo audioDeviceInfo = getRoutedDevice();
+        if (audioDeviceInfo != null) {
+            audioDeviceInfos.add(audioDeviceInfo);
+        }
+        return audioDeviceInfos;
+    }
+
     /*
      * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
      */
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 7895eb2..5b1ea8b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -78,13 +78,6 @@
 }
 
 flag {
-    name: "update_client_profile_priority"
-    namespace: "media_solutions"
-    description : "Feature flag to add updateResourcePriority api to MediaCas"
-    bug: "300565729"
-}
-
-flag {
      name: "enable_built_in_speaker_route_suitability_statuses"
      is_exported: true
      namespace: "media_solutions"
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 17d1ff6..1bb9a8e 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -18,3 +18,10 @@
     bug: "362720120"
     is_exported: true
 }
+
+flag {
+     namespace: "media_projection"
+     name: "stop_media_projection_on_call_end"
+     description: "Stops MediaProjection sessions when a call ends"
+     bug: "368336349"
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index dd2a534..ff0279f 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -240,6 +240,38 @@
         }
     }
 
+    /**
+     * Controls whether the TvAdView's surface is placed on top of other regular surface views in
+     * the window (but still behind the window itself).
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
+     *
+     * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false}
+     *            otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
+    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setZOrderOnTop(false);
+            mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
+        }
+    }
+
+    /**
+     * Controls whether the TvAdView's surface is placed on top of its window.
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
+     *
+     * @param onTop {@code true} to be on top of its window, {@code false} otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
+    public void setZOrderOnTop(boolean onTop) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setZOrderMediaOverlay(false);
+            mSurfaceView.setZOrderOnTop(onTop);
+        }
+    }
+
     private void resetSurfaceView() {
         if (mSurfaceView != null) {
             mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
diff --git a/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl b/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl
new file mode 100644
index 0000000..2a2e71a
--- /dev/null
+++ b/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.extension.oad;
+
+/**
+ * @hide
+ */
+interface IOadUpdateInterface {
+    // Enable or disable the OAD function.
+    void setOadStatus(boolean enable);
+    // Get status of OAD function.
+    boolean getOadStatus();
+    // Start OAD scan of all frequency in the program list.
+    void startScan();
+    // Stop OAD scan of all frequency in the program list.
+    void stopScan();
+    // Start OAD detect for the current channel.
+    void startDetect();
+    // Stop OAD detect for the current channel.
+    void stopDetect();
+    // Start OAD download after it has been detected or scanned.
+    void startDownload();
+    // Stop OAD download.
+    void stopDownload();
+    // Retrieves current OAD software version.
+    int getSoftwareVersion();
+}
diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/rating/IDownloadableRatingTableMonitor.aidl
similarity index 62%
rename from core/java/android/text/ClientFlags.java
rename to media/java/android/media/tv/extension/rating/IDownloadableRatingTableMonitor.aidl
index ca88764..bf1a385 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/media/java/android/media/tv/extension/rating/IDownloadableRatingTableMonitor.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,14 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.media.tv.extension.rating;
+
+import android.os.Bundle;
 
 /**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
  * @hide
  */
-public class ClientFlags {
+interface IDownloadableRatingTableMonitor {
+    // Get RRT rating info on downloadable rating data
+    Bundle[] getTable();
 }
diff --git a/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl
new file mode 100644
index 0000000..06cac3d
--- /dev/null
+++ b/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.extension.rating;
+
+import android.media.tv.extension.rating.IPmtRatingListener;
+
+/**
+ * @hide
+ */
+interface IPmtRatingInterface {
+    // Get Pmt rating information.
+    String getPmtRating(String sessionToken);
+    // Register a listener for pmt rating updates.
+    void addPmtRatingListener(String clientToken, in IPmtRatingListener listener);
+    // Remove the previously added IPmtRatingListener.
+    void removePmtRatingListener(in IPmtRatingListener listener);
+}
diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl
similarity index 62%
copy from core/java/android/text/ClientFlags.java
copy to media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl
index ca88764..d88ae94 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,11 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.media.tv.extension.rating;
 
 /**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
  * @hide
  */
-public class ClientFlags {
+oneway interface IPmtRatingListener {
+    void onPmtRatingChanged(String sessionToken, String newTvContentRating);
 }
diff --git a/media/java/android/media/tv/extension/rating/IProgramRatingInfo.aidl b/media/java/android/media/tv/extension/rating/IProgramRatingInfo.aidl
new file mode 100644
index 0000000..a490491
--- /dev/null
+++ b/media/java/android/media/tv/extension/rating/IProgramRatingInfo.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.media.tv.extension.rating;
+
+import android.media.tv.extension.rating.IProgramRatingInfoListener;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+interface IProgramRatingInfo {
+    // Register a listener to receive notifications when ProgramRatingInfo is updated.
+    void addProgramRatingInfoListener(String clientToken, in IProgramRatingInfoListener listener);
+    // Remove a listener for ProgramRatingInfo update notifications.
+    void removeProgramRatingInfoListener(in IProgramRatingInfoListener listener);
+    // Get ProgramRatingInfo that may only be obtained when viewing.
+    Bundle getProgramRatingInfo(String sessionToken);
+}
diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl
similarity index 62%
copy from core/java/android/text/ClientFlags.java
copy to media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl
index ca88764..6777cd3 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,13 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.media.tv.extension.rating;
+
+import android.os.Bundle;
 
 /**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
  * @hide
  */
-public class ClientFlags {
+interface IProgramRatingInfoListener {
+    void onProgramInfoChanged(String sessionToken,in Bundle changedProgramInfo);
 }
diff --git a/media/java/android/media/tv/extension/rating/IRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IRatingInterface.aidl
new file mode 100644
index 0000000..d68fe76
--- /dev/null
+++ b/media/java/android/media/tv/extension/rating/IRatingInterface.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.extension.rating;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+interface IRatingInterface {
+    // Get RRT rating information
+    Bundle getRRTRatingInfo();
+    // Set RRT rating information when user select
+    boolean setRRTRatingInfo(in Bundle param);
+    // Reset RRT5 to clear information
+    boolean setResetRrt5();
+}
diff --git a/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl
new file mode 100644
index 0000000..bad4067
--- /dev/null
+++ b/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.extension.rating;
+
+import android.media.tv.extension.rating.IVbiRatingListener;
+
+/**
+ * @hide
+ */
+interface IVbiRatingInterface {
+    // Get Vbi rating.
+    String getVbiRating(String sessionToken);
+    // Register a listener for Vbi rating updates.
+    void addVbiRatingListener(String clientToken, in IVbiRatingListener listener);
+    // Remove the previously added VbiRatingListener.
+    void removeVbiRatingListener(in IVbiRatingListener listener);
+}
diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl
similarity index 62%
copy from core/java/android/text/ClientFlags.java
copy to media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl
index ca88764..36d523f 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,11 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.media.tv.extension.rating;
 
 /**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
  * @hide
  */
-public class ClientFlags {
+oneway interface IVbiRatingListener {
+    void onVbiRatingChanged(String sessionToken, String newTvContentRating);
 }
diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/time/IBroadcastTime.aidl
similarity index 61%
copy from core/java/android/text/ClientFlags.java
copy to media/java/android/media/tv/extension/time/IBroadcastTime.aidl
index ca88764..123d00f 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/media/java/android/media/tv/extension/time/IBroadcastTime.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.media.tv.extension.time;
+
+import android.os.Bundle;
 
 /**
- * An aconfig feature flags that can be accessible from application process without
- * ContentProvider IPCs.
- *
- * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
- *
- * TODO(nona): Remove this class.
  * @hide
  */
-public class ClientFlags {
-}
+interface IBroadcastTime {
+    long getUtcTime();
+    long getLocalTime();
+    Bundle getTimeZoneInfo();
+    long getUtcTimePerStream(String SessionToken);
+    long getLocalTimePerStream(String SessionToken);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 6441652..4b832ae 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -2,6 +2,22 @@
 container: "system"
 
 flag {
+    name: "enable_le_audio_broadcast_ui"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Enable Broadcast UI for LE Audio on TV."
+    bug: "378732734"
+}
+
+flag {
+    name: "enable_le_audio_unicast_ui"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Enable Unicast UI for LE Audio on TV."
+    bug: "378732734"
+}
+
+flag {
     name: "broadcast_visibility_types"
     is_exported: true
     namespace: "media_tv"
@@ -77,11 +93,19 @@
     name: "set_resource_holder_retain"
     is_exported: true
     namespace: "media_tv"
-    description : "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA."
+    description: "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA."
     bug: "372973197"
 }
 
 flag {
+    name: "mediacas_update_client_profile_priority"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Feature flag to add updateResourcePriority api to MediaCas"
+    bug: "372971241"
+}
+
+flag {
     name: "apply_picture_profiles"
     is_exported: true
     namespace: "media_tv"
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 635572d..9e9699f 100644
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -271,6 +271,38 @@
         }
     }
 
+    /**
+     * Controls whether the TvInteractiveAppView's surface is placed on top of other regular surface
+     * views in the window (but still behind the window itself).
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
+     *
+     * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false}
+     *            otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setZOrderOnTop(false);
+            mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
+        }
+    }
+
+    /**
+     * Controls whether the TvInterActiveAppView's surface is placed on top of its window.
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
+     *
+     * @param onTop {@code true} to be on top of its window, {@code false} otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+    public void setZOrderOnTop(boolean onTop) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setZOrderMediaOverlay(false);
+            mSurfaceView.setZOrderOnTop(onTop);
+        }
+    }
+
     private void resetSurfaceView() {
         if (mSurfaceView != null) {
             mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index ac85ab7..88c1c43 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -257,6 +257,16 @@
         public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) {
             // TODO Auto-generated method stub
         }
+
+        /*
+         * (non-Javadoc)
+         * @see android.hardware.camera2.ICameraDeviceCallbacks#onClientSharedAccessPriorityChanged
+         */
+        @Override
+        public void onClientSharedAccessPriorityChanged(boolean primaryClient) {
+            // TODO Auto-generated method stub
+        }
+
     }
 
     @SmallTest
@@ -276,7 +286,7 @@
                         0 /*oomScoreOffset*/,
                         getContext().getApplicationInfo().targetSdkVersion,
                         ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution,
-                        DEVICE_POLICY_DEFAULT);
+                        DEVICE_POLICY_DEFAULT, false/*sharedMode*/);
             assertNotNull(String.format("Camera %s was null", cameraId), cameraUser);
 
             Log.v(TAG, String.format("Camera %s connected", cameraId));
@@ -320,6 +330,13 @@
             Log.v(TAG, String.format("Camera " + cameraId + " torch strength level changed to "
                     + torchStrength ));
         }
+        @Override
+        public void onCameraOpenedInSharedMode(String cameraId, String clientPackageName,
+                int deviceId, boolean primaryClient) {
+            Log.v(TAG, "Camera " + cameraId +  " is opened in shared mode by "
+                    + "client package "  + clientPackageName + " as primary client="
+                    + primaryClient);
+        }
     }
 
     /**
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
index 35ad924..3758c51 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
@@ -175,6 +175,15 @@
         public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) {
             // TODO Auto-generated method stub
         }
+
+        /**
+         * (non-Javadoc)
+         * @see android.hardware.camera2.ICameraDeviceCallbacks#onClientSharedAccessPriorityChanged
+         */
+        @Override
+        public void onClientSharedAccessPriorityChanged(boolean primaryClient) {
+            // TODO Auto-generated method stub
+        }
     }
 
     class IsMetadataNotEmpty implements ArgumentMatcher<CameraMetadataNative> {
@@ -250,7 +259,8 @@
 
         mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId,
                 /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion,
-                ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT);
+                ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT,
+                /*sharedMode*/false);
         assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser);
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index a046057..2d1fbf9 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -361,6 +361,8 @@
     APerformanceHint_setThreads; # introduced=UpsideDownCake
     APerformanceHint_setPreferPowerEfficiency; # introduced=VanillaIceCream
     APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream
+    APerformanceHint_notifyWorkloadIncrease; # introduced=36
+    APerformanceHint_notifyWorkloadReset; # introduced=36
     AWorkDuration_create; # introduced=VanillaIceCream
     AWorkDuration_release; # introduced=VanillaIceCream
     AWorkDuration_setWorkPeriodStartTimestampNanos; # introduced=VanillaIceCream
@@ -379,6 +381,8 @@
     APerformanceHint_getThreadIds;
     APerformanceHint_createSessionInternal;
     APerformanceHint_setUseFMQForTesting;
+    APerformanceHint_getRateLimiterPropertiesForTesting;
+    APerformanceHint_setUseNewLoadHintBehaviorForTesting;
     extern "C++" {
         ASurfaceControl_registerSurfaceStatsListener*;
         ASurfaceControl_unregisterSurfaceStatsListener*;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 15f77ce..e2fa94d 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -33,12 +33,14 @@
 #include <android/performance_hint.h>
 #include <android/trace.h>
 #include <android_os.h>
+#include <cutils/trace.h>
 #include <fmq/AidlMessageQueue.h>
 #include <inttypes.h>
 #include <performance_hint_private.h>
 #include <utils/SystemClock.h>
 
 #include <chrono>
+#include <format>
 #include <future>
 #include <set>
 #include <utility>
@@ -63,6 +65,22 @@
 constexpr int64_t SEND_HINT_TIMEOUT = std::chrono::nanoseconds(100ms).count();
 struct AWorkDuration : public hal::WorkDuration {};
 
+// A pair of values that determine the behavior of the
+// load hint rate limiter, to only allow "X hints every Y seconds"
+constexpr double kLoadHintInterval = std::chrono::nanoseconds(2s).count();
+constexpr double kMaxLoadHintsPerInterval = 20;
+constexpr double kReplenishRate = kMaxLoadHintsPerInterval / kLoadHintInterval;
+bool kForceNewHintBehavior = false;
+
+template <class T>
+constexpr int32_t enum_size() {
+    return static_cast<int32_t>(*(ndk::enum_range<T>().end() - 1)) + 1;
+}
+
+bool useNewLoadHintBehavior() {
+    return android::os::adpf_use_load_hints() || kForceNewHintBehavior;
+}
+
 // Shared lock for the whole PerformanceHintManager and sessions
 static std::mutex sHintMutex = std::mutex{};
 class FMQWrapper {
@@ -76,7 +94,8 @@
                                    hal::WorkDuration* durations, size_t count) REQUIRES(sHintMutex);
     bool updateTargetWorkDuration(std::optional<hal::SessionConfig>& config,
                                   int64_t targetDurationNanos) REQUIRES(sHintMutex);
-    bool sendHint(std::optional<hal::SessionConfig>& config, SessionHint hint) REQUIRES(sHintMutex);
+    bool sendHints(std::optional<hal::SessionConfig>& config, std::vector<hal::SessionHint>& hint,
+                   int64_t now) REQUIRES(sHintMutex);
     bool setMode(std::optional<hal::SessionConfig>& config, hal::SessionMode, bool enabled)
             REQUIRES(sHintMutex);
     void setToken(ndk::SpAIBinder& token);
@@ -86,10 +105,11 @@
 private:
     template <HalChannelMessageContents::Tag T, bool urgent = false,
               class C = HalChannelMessageContents::_at<T>>
-    bool sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count = 1)
-            REQUIRES(sHintMutex);
+    bool sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count = 1,
+                      int64_t now = ::android::uptimeNanos()) REQUIRES(sHintMutex);
     template <HalChannelMessageContents::Tag T, class C = HalChannelMessageContents::_at<T>>
-    void writeBuffer(C* message, hal::SessionConfig& config, size_t count) REQUIRES(sHintMutex);
+    void writeBuffer(C* message, hal::SessionConfig& config, size_t count, int64_t now)
+            REQUIRES(sHintMutex);
 
     bool isActiveLocked() REQUIRES(sHintMutex);
     bool updatePersistentTransaction() REQUIRES(sHintMutex);
@@ -120,6 +140,7 @@
                                            hal::SessionTag tag = hal::SessionTag::APP);
     int64_t getPreferredRateNanos() const;
     FMQWrapper& getFMQWrapper();
+    bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex);
 
 private:
     // Necessary to create an empty binder object
@@ -138,6 +159,8 @@
     ndk::SpAIBinder mToken;
     const int64_t mPreferredRateNanos;
     FMQWrapper mFMQWrapper;
+    double mHintBudget = kMaxLoadHintsPerInterval;
+    int64_t mLastBudgetReplenish = 0;
 };
 
 struct APerformanceHintSession {
@@ -151,7 +174,9 @@
 
     int updateTargetWorkDuration(int64_t targetDurationNanos);
     int reportActualWorkDuration(int64_t actualDurationNanos);
-    int sendHint(SessionHint hint);
+    int sendHints(std::vector<hal::SessionHint>& hints, int64_t now, const char* debugName);
+    int notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName);
+    int notifyWorkloadReset(bool cpu, bool gpu, const char* debugName);
     int setThreads(const int32_t* threadIds, size_t size);
     int getThreadIds(int32_t* const threadIds, size_t* size);
     int setPreferPowerEfficiency(bool enabled);
@@ -173,6 +198,8 @@
     // Last target hit timestamp
     int64_t mLastTargetMetTimestamp GUARDED_BY(sHintMutex);
     // Last hint reported from sendHint indexed by hint value
+    // This is only used by the old rate limiter impl and is replaced
+    // with the new rate limiter under a flag
     std::vector<int64_t> mLastHintSentTimestamp GUARDED_BY(sHintMutex);
     // Cached samples
     std::vector<hal::WorkDuration> mActualWorkDurations GUARDED_BY(sHintMutex);
@@ -255,6 +282,21 @@
     return new APerformanceHintManager(manager, preferredRateNanos);
 }
 
+bool APerformanceHintManager::canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) {
+    mHintBudget =
+            std::max(kMaxLoadHintsPerInterval,
+                     mHintBudget +
+                             static_cast<double>(now - mLastBudgetReplenish) * kReplenishRate);
+    mLastBudgetReplenish = now;
+
+    // If this youngest timestamp isn't older than the timeout time, we can't send
+    if (hints.size() > mHintBudget) {
+        return false;
+    }
+    mHintBudget -= hints.size();
+    return true;
+}
+
 APerformanceHintSession* APerformanceHintManager::createSession(
         const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos,
         hal::SessionTag tag) {
@@ -292,9 +334,7 @@
 
 // ===================================== APerformanceHintSession implementation
 
-constexpr int kNumEnums =
-        ndk::enum_range<hal::SessionHint>().end() - ndk::enum_range<hal::SessionHint>().begin();
-
+constexpr int kNumEnums = enum_size<hal::SessionHint>();
 APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> hintManager,
                                                  std::shared_ptr<IHintSession> session,
                                                  int64_t preferredRateNanos,
@@ -361,31 +401,83 @@
     return reportActualWorkDurationInternal(static_cast<AWorkDuration*>(&workDuration));
 }
 
-int APerformanceHintSession::sendHint(SessionHint hint) {
+int APerformanceHintSession::sendHints(std::vector<hal::SessionHint>& hints, int64_t now,
+                                       const char*) {
     std::scoped_lock lock(sHintMutex);
-    if (hint < 0 || hint >= static_cast<int32_t>(mLastHintSentTimestamp.size())) {
-        ALOGE("%s: invalid session hint %d", __FUNCTION__, hint);
+    if (hints.empty()) {
         return EINVAL;
     }
-    int64_t now = uptimeNanos();
-
-    // Limit sendHint to a pre-detemined rate for safety
-    if (now < (mLastHintSentTimestamp[hint] + SEND_HINT_TIMEOUT)) {
-        return 0;
-    }
-
-    if (!getFMQ().sendHint(mSessionConfig, hint)) {
-        ndk::ScopedAStatus ret = mHintSession->sendHint(hint);
-
-        if (!ret.isOk()) {
-            ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.getMessage());
-            return EPIPE;
+    for (auto&& hint : hints) {
+        if (static_cast<int32_t>(hint) < 0 || static_cast<int32_t>(hint) >= kNumEnums) {
+            ALOGE("%s: invalid session hint %d", __FUNCTION__, hint);
+            return EINVAL;
         }
     }
-    mLastHintSentTimestamp[hint] = now;
+
+    if (useNewLoadHintBehavior()) {
+        if (!APerformanceHintManager::getInstance()->canSendLoadHints(hints, now)) {
+            return EBUSY;
+        }
+    }
+    // keep old rate limiter behavior for legacy flag
+    else {
+        for (auto&& hint : hints) {
+            if (now < (mLastHintSentTimestamp[static_cast<int32_t>(hint)] + SEND_HINT_TIMEOUT)) {
+                return EBUSY;
+            }
+        }
+    }
+
+    if (!getFMQ().sendHints(mSessionConfig, hints, now)) {
+        for (auto&& hint : hints) {
+            ndk::ScopedAStatus ret = mHintSession->sendHint(static_cast<int32_t>(hint));
+
+            if (!ret.isOk()) {
+                ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.getMessage());
+                return EPIPE;
+            }
+        }
+    }
+
+    if (!useNewLoadHintBehavior()) {
+        for (auto&& hint : hints) {
+            mLastHintSentTimestamp[static_cast<int32_t>(hint)] = now;
+        }
+    }
+
+    if (ATrace_isEnabled()) {
+        ATRACE_INSTANT("Sending load hint");
+    }
+
     return 0;
 }
 
+int APerformanceHintSession::notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName) {
+    std::vector<hal::SessionHint> hints(2);
+    hints.clear();
+    if (cpu) {
+        hints.push_back(hal::SessionHint::CPU_LOAD_UP);
+    }
+    if (gpu) {
+        hints.push_back(hal::SessionHint::GPU_LOAD_UP);
+    }
+    int64_t now = ::android::uptimeNanos();
+    return sendHints(hints, now, debugName);
+}
+
+int APerformanceHintSession::notifyWorkloadReset(bool cpu, bool gpu, const char* debugName) {
+    std::vector<hal::SessionHint> hints(2);
+    hints.clear();
+    if (cpu) {
+        hints.push_back(hal::SessionHint::CPU_LOAD_RESET);
+    }
+    if (gpu) {
+        hints.push_back(hal::SessionHint::GPU_LOAD_RESET);
+    }
+    int64_t now = ::android::uptimeNanos();
+    return sendHints(hints, now, debugName);
+}
+
 int APerformanceHintSession::setThreads(const int32_t* threadIds, size_t size) {
     if (size == 0) {
         ALOGE("%s: the list of thread ids must not be empty.", __FUNCTION__);
@@ -565,24 +657,25 @@
 }
 
 template <HalChannelMessageContents::Tag T, class C>
-void FMQWrapper::writeBuffer(C* message, hal::SessionConfig& config, size_t) {
-    new (mFmqTransaction.getSlot(0)) hal::ChannelMessage{
-            .sessionID = static_cast<int32_t>(config.id),
-            .timeStampNanos = ::android::uptimeNanos(),
-            .data = HalChannelMessageContents::make<T, C>(std::move(*message)),
-    };
+void FMQWrapper::writeBuffer(C* message, hal::SessionConfig& config, size_t count, int64_t now) {
+    for (size_t i = 0; i < count; ++i) {
+        new (mFmqTransaction.getSlot(i)) hal::ChannelMessage{
+                .sessionID = static_cast<int32_t>(config.id),
+                .timeStampNanos = now,
+                .data = HalChannelMessageContents::make<T, C>(std::move(*(message + i))),
+        };
+    }
 }
 
 template <>
 void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkDuration* messages,
                                                                       hal::SessionConfig& config,
-                                                                      size_t count) {
+                                                                      size_t count, int64_t now) {
     for (size_t i = 0; i < count; ++i) {
         hal::WorkDuration& message = messages[i];
         new (mFmqTransaction.getSlot(i)) hal::ChannelMessage{
                 .sessionID = static_cast<int32_t>(config.id),
-                .timeStampNanos =
-                        (i == count - 1) ? ::android::uptimeNanos() : message.timeStampNanos,
+                .timeStampNanos = (i == count - 1) ? now : message.timeStampNanos,
                 .data = HalChannelMessageContents::make<HalChannelMessageContents::workDuration,
                                                         hal::WorkDurationFixedV1>({
                         .durationNanos = message.cpuDurationNanos,
@@ -595,7 +688,8 @@
 }
 
 template <HalChannelMessageContents::Tag T, bool urgent, class C>
-bool FMQWrapper::sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count) {
+bool FMQWrapper::sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count,
+                              int64_t now) {
     if (!isActiveLocked() || !config.has_value() || mCorrupted) {
         return false;
     }
@@ -609,7 +703,7 @@
             return false;
         }
     }
-    writeBuffer<T, C>(message, *config, count);
+    writeBuffer<T, C>(message, *config, count, now);
     mQueue->commitWrite(count);
     mEventFlag->wake(mWriteMask);
     // Re-create the persistent transaction after writing
@@ -641,10 +735,9 @@
     return sendMessages<HalChannelMessageContents::targetDuration>(config, &targetDurationNanos);
 }
 
-bool FMQWrapper::sendHint(std::optional<hal::SessionConfig>& config, SessionHint hint) {
-    return sendMessages<HalChannelMessageContents::hint>(config,
-                                                         reinterpret_cast<hal::SessionHint*>(
-                                                                 &hint));
+bool FMQWrapper::sendHints(std::optional<hal::SessionConfig>& config,
+                           std::vector<hal::SessionHint>& hints, int64_t now) {
+    return sendMessages<HalChannelMessageContents::hint>(config, hints.data(), hints.size(), now);
 }
 
 bool FMQWrapper::setMode(std::optional<hal::SessionConfig>& config, hal::SessionMode mode,
@@ -758,7 +851,9 @@
 
 int APerformanceHint_sendHint(APerformanceHintSession* session, SessionHint hint) {
     VALIDATE_PTR(session)
-    return session->sendHint(hint);
+    std::vector<hal::SessionHint> hints{static_cast<hal::SessionHint>(hint)};
+    int64_t now = ::android::uptimeNanos();
+    return session->sendHints(hints, now, "HWUI hint");
 }
 
 int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds,
@@ -791,6 +886,26 @@
     return session->reportActualWorkDuration(workDurationPtr);
 }
 
+int APerformanceHint_notifyWorkloadIncrease(APerformanceHintSession* session, bool cpu, bool gpu,
+                                            const char* debugName) {
+    VALIDATE_PTR(session)
+    VALIDATE_PTR(debugName)
+    if (!useNewLoadHintBehavior()) {
+        return ENOTSUP;
+    }
+    return session->notifyWorkloadIncrease(cpu, gpu, debugName);
+}
+
+int APerformanceHint_notifyWorkloadReset(APerformanceHintSession* session, bool cpu, bool gpu,
+                                         const char* debugName) {
+    VALIDATE_PTR(session)
+    VALIDATE_PTR(debugName)
+    if (!useNewLoadHintBehavior()) {
+        return ENOTSUP;
+    }
+    return session->notifyWorkloadReset(cpu, gpu, debugName);
+}
+
 AWorkDuration* AWorkDuration_create() {
     return new AWorkDuration();
 }
@@ -838,3 +953,13 @@
 void APerformanceHint_setUseFMQForTesting(bool enabled) {
     gForceFMQEnabled = enabled;
 }
+
+void APerformanceHint_getRateLimiterPropertiesForTesting(int32_t* maxLoadHintsPerInterval,
+                                                         int64_t* loadHintInterval) {
+    *maxLoadHintsPerInterval = kMaxLoadHintsPerInterval;
+    *loadHintInterval = kLoadHintInterval;
+}
+
+void APerformanceHint_setUseNewLoadHintBehaviorForTesting(bool newBehavior) {
+    kForceNewHintBehavior = newBehavior;
+}
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 9de3a6f..f707a0e 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -66,6 +66,18 @@
                  std::optional<hal::ChannelConfig>* _aidl_return),
                 (override));
     MOCK_METHOD(ScopedAStatus, closeSessionChannel, (), (override));
+    MOCK_METHOD(ScopedAStatus, getCpuHeadroom,
+                (const ::aidl::android::os::CpuHeadroomParamsInternal& in_params,
+                 std::vector<float>* _aidl_return),
+                (override));
+    MOCK_METHOD(ScopedAStatus, getCpuHeadroomMinIntervalMillis, (int64_t* _aidl_return),
+                (override));
+    MOCK_METHOD(ScopedAStatus, getGpuHeadroom,
+                (const ::aidl::android::os::GpuHeadroomParamsInternal& in_params,
+                 float* _aidl_return),
+                (override));
+    MOCK_METHOD(ScopedAStatus, getGpuHeadroomMinIntervalMillis, (int64_t* _aidl_return),
+                (override));
     MOCK_METHOD(SpAIBinder, asBinder, (), (override));
     MOCK_METHOD(bool, isRemote, (), (override));
 };
@@ -90,7 +102,10 @@
 public:
     void SetUp() override {
         mMockIHintManager = ndk::SharedRefBase::make<NiceMock<MockIHintManager>>();
+        APerformanceHint_getRateLimiterPropertiesForTesting(&mMaxLoadHintsPerInterval,
+                                                            &mLoadHintInterval);
         APerformanceHint_setIHintManagerForTesting(&mMockIHintManager);
+        APerformanceHint_setUseNewLoadHintBehaviorForTesting(true);
     }
 
     void TearDown() override {
@@ -176,6 +191,9 @@
     int kMockQueueSize = 20;
     bool mUsingFMQ = false;
 
+    int32_t mMaxLoadHintsPerInterval;
+    int64_t mLoadHintInterval;
+
     template <HalChannelMessageContents::Tag T, class C = HalChannelMessageContents::_at<T>>
     void expectToReadFromFmq(C expected) {
         hal::ChannelMessage readData;
@@ -218,7 +236,6 @@
     EXPECT_CALL(*mMockSession, reportActualWorkDuration2(_)).Times(Exactly(1));
     result = APerformanceHint_reportActualWorkDuration(session, actualDurationNanos);
     EXPECT_EQ(0, result);
-
     result = APerformanceHint_updateTargetWorkDuration(session, -1L);
     EXPECT_EQ(EINVAL, result);
     result = APerformanceHint_reportActualWorkDuration(session, -1L);
@@ -228,18 +245,28 @@
     EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(1));
     result = APerformanceHint_sendHint(session, hintId);
     EXPECT_EQ(0, result);
-    usleep(110000); // Sleep for longer than the update timeout.
-    EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(1));
-    result = APerformanceHint_sendHint(session, hintId);
+    EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::CPU_LOAD_UP))).Times(Exactly(1));
+    result = APerformanceHint_notifyWorkloadIncrease(session, true, false, "Test hint");
     EXPECT_EQ(0, result);
-    // Expect to get rate limited if we try to send faster than the limiter allows
-    EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(0));
-    result = APerformanceHint_sendHint(session, hintId);
+    EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::CPU_LOAD_RESET))).Times(Exactly(1));
+    EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::GPU_LOAD_RESET))).Times(Exactly(1));
+    result = APerformanceHint_notifyWorkloadReset(session, true, true, "Test hint");
     EXPECT_EQ(0, result);
 
     result = APerformanceHint_sendHint(session, static_cast<SessionHint>(-1));
     EXPECT_EQ(EINVAL, result);
 
+    Mock::VerifyAndClearExpectations(mMockSession.get());
+    for (int i = 0; i < mMaxLoadHintsPerInterval; ++i) {
+        APerformanceHint_sendHint(session, hintId);
+    }
+
+    // Expect to get rate limited if we try to send faster than the limiter allows
+    EXPECT_CALL(*mMockSession, sendHint(_)).Times(Exactly(0));
+    result = APerformanceHint_notifyWorkloadIncrease(session, true, true, "Test hint");
+    EXPECT_EQ(result, EBUSY);
+    EXPECT_CALL(*mMockSession, sendHint(_)).Times(Exactly(0));
+    result = APerformanceHint_notifyWorkloadReset(session, true, true, "Test hint");
     EXPECT_CALL(*mMockSession, close()).Times(Exactly(1));
     APerformanceHint_closeSession(session);
 }
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index c25f77b..79a0607 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -89,12 +89,12 @@
     method public void onBootFinished(int);
     method public void onBootStarted();
     method public void onCardEmulationActivated(boolean);
-    method public void onDisable(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onDisableFinished(int);
+    method public void onDisableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onDisableStarted();
     method public void onEeListenActivated(boolean);
-    method public void onEnable(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onEnableFinished(int);
+    method public void onEnableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onEnableStarted();
     method public void onGetOemAppSearchIntent(@NonNull java.util.List<java.lang.String>, @NonNull java.util.function.Consumer<android.content.Intent>);
     method public void onHceEventReceived(int);
@@ -183,6 +183,12 @@
     method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int);
     method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void overrideRoutingTable(@NonNull android.app.Activity, int, int);
     method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void recoverRoutingTable(@NonNull android.app.Activity);
+    method @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setServiceEnabledForCategoryOther(@NonNull android.content.ComponentName, boolean);
+    field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET = 3; // 0x3
+    field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED = 1; // 0x1
+    field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE = 2; // 0x2
+    field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR = 4; // 0x4
+    field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_OK = 0; // 0x0
   }
 
 }
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 5e2e92d..633d8bf 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -47,7 +47,7 @@
     boolean unsetPreferredService();
     boolean supportsAidPrefixRegistration();
     ApduServiceInfo getPreferredPaymentService(int userHandle);
-    boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status);
+    int setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status);
     boolean isDefaultPaymentRegistered();
 
     void overrideRoutingTable(int userHandle, String protocol, String technology, in String pkg);
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 57ee981..c677cd6 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -27,7 +27,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
-import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.content.ComponentName;
 import android.content.Context;
@@ -233,8 +232,7 @@
          *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
          * false if NFC cannot be enabled at this time.
          */
-        @SuppressLint("MethodNameTense")
-        void onEnable(@NonNull Consumer<Boolean> isAllowed);
+        void onEnableRequested(@NonNull Consumer<Boolean> isAllowed);
         /**
          * Method to check if Nfc is allowed to be disabled by OEMs.
          * @param isAllowed The {@link Consumer} to be completed. If disabling NFC is allowed,
@@ -242,7 +240,7 @@
          *                  {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
          * false if NFC cannot be disabled at this time.
          */
-        void onDisable(@NonNull Consumer<Boolean> isAllowed);
+        void onDisableRequested(@NonNull Consumer<Boolean> isAllowed);
 
         /**
          * Callback to indicate that Nfc starts to boot.
@@ -255,7 +253,7 @@
         void onEnableStarted();
 
         /**
-         * Callback to indicate that Nfc starts to enable.
+         * Callback to indicate that Nfc starts to disable.
          */
         void onDisableStarted();
 
@@ -799,13 +797,13 @@
         public void onEnable(ResultReceiver isAllowed) throws RemoteException {
             mCallbackMap.forEach((cb, ex) ->
                     handleVoidCallback(
-                        new ReceiverWrapper<>(isAllowed), cb::onEnable, ex));
+                        new ReceiverWrapper<>(isAllowed), cb::onEnableRequested, ex));
         }
         @Override
         public void onDisable(ResultReceiver isAllowed) throws RemoteException {
             mCallbackMap.forEach((cb, ex) ->
                     handleVoidCallback(
-                        new ReceiverWrapper<>(isAllowed), cb::onDisable, ex));
+                        new ReceiverWrapper<>(isAllowed), cb::onDisableRequested, ex));
         }
         @Override
         public void onBootStarted() throws RemoteException {
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index eb28c3b..8917524 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -185,6 +185,65 @@
     @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
     public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET = -1;
 
+    /**
+     * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * succeeded.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    public static final int SET_SERVICE_ENABLED_STATUS_OK = 0;
+
+    /**
+     * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * failed due to the unsupported feature.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED = 1;
+
+    /**
+     * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * failed due to the invalid service.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE = 2;
+
+    /**
+     * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * failed due to the service is already set to the requested status.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET = 3;
+
+    /**
+     * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * failed due to unknown error.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR = 4;
+
+    /**
+     * Status code returned by {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
+     * @hide
+     */
+    @IntDef(prefix = "SET_SERVICE_ENABLED_STATUS_", value = {
+            SET_SERVICE_ENABLED_STATUS_OK,
+            SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED,
+            SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE,
+            SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET,
+            SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SetServiceEnabledStatusCode {}
+
     static boolean sIsInitialized = false;
     static HashMap<Context, CardEmulation> sCardEmus = new HashMap<Context, CardEmulation>();
     static INfcCardEmulation sService;
@@ -883,22 +942,24 @@
     }
 
     /**
-     * Allows to set or unset preferred service (category other) to avoid  AID Collision.
+     * Allows to set or unset preferred service (category other) to avoid AID Collision. The user
+     * should use corresponding context using {@link Context#createContextAsUser(UserHandle, int)}
      *
      * @param service The ComponentName of the service
      * @param status  true to enable, false to disable
-     * @param userId the user handle of the user whose information is being requested.
-     * @return set service for the category and true if service is already set return false.
+     * @return true if preferred service is successfully set or unset, otherwise return false.
      *
      * @hide
      */
-    public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status,
-                                                     int userId) {
-        if (service == null) {
-            throw new NullPointerException("activity or service or category is null");
-        }
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
+    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+    @SetServiceEnabledStatusCode
+    public int setServiceEnabledForCategoryOther(@NonNull ComponentName service,
+            boolean status) {
         return callServiceReturn(() ->
-                sService.setServiceEnabledForCategoryOther(userId, service, status), false);
+                sService.setServiceEnabledForCategoryOther(mContext.getUser().getIdentifier(),
+                        service, status), SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR);
     }
 
     /** @hide */
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index 34f0200..8a37aa2 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -173,3 +173,11 @@
     description: "Share wallet role routing priority with associated services"
     bug: "366243361"
 }
+
+flag {
+    name: "nfc_set_service_enabled_for_category_other"
+    is_exported: true
+    namespace: "nfc"
+    description: "Enable set service enabled for category other"
+    bug: "338157113"
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
index 5ceee6d..088bef2 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -22,11 +22,13 @@
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.MessageCodec
 import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenProvider
 import java.util.Locale
 
 /** API to get preference graph. */
-abstract class GetPreferenceGraphApiHandler :
-    ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
+abstract class GetPreferenceGraphApiHandler(
+    private val preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>>
+) : ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
 
     override val requestCodec: MessageCodec<GetPreferenceGraphRequest>
         get() = GetPreferenceGraphRequestCodec
@@ -40,14 +42,16 @@
         callingUid: Int,
         request: GetPreferenceGraphRequest,
     ): PreferenceGraphProto {
-        val builderRequest =
-            if (request.screenKeys.isEmpty()) {
-                val keys = PreferenceScreenRegistry.preferenceScreens.keys
-                GetPreferenceGraphRequest(keys, request.visitedScreens, request.locale)
-            } else {
-                request
+        val builder = PreferenceGraphBuilder.of(application, request)
+        if (request.screenKeys.isEmpty()) {
+            for (key in PreferenceScreenRegistry.preferenceScreens.keys) {
+                builder.addPreferenceScreenFromRegistry(key)
             }
-        return PreferenceGraphBuilder.of(application, builderRequest).build()
+            for (provider in preferenceScreenProviders) {
+                builder.addPreferenceScreenProvider(provider)
+            }
+        }
+        return builder.build()
     }
 }
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index 2256bb3..6760e72 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -133,7 +133,7 @@
             null
         }
 
-    private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
+    suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
         val metadata = PreferenceScreenRegistry[key] ?: return false
         return addPreferenceScreenMetadata(metadata)
     }
@@ -146,7 +146,7 @@
             }
         }
 
-    private suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
+    suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
         Log.d(TAG, "add $activityClass")
         createPreferenceScreen { activityClass.newInstance() }
             ?.let {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index 6e4db1d..7cfce0d 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.graph.proto.PreferenceValueProto
 import com.android.settingslib.ipc.ApiDescriptor
 import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.ApiPermissionChecker
 import com.android.settingslib.ipc.IntMessageCodec
 import com.android.settingslib.ipc.MessageCodec
 import com.android.settingslib.metadata.BooleanValue
@@ -45,7 +46,11 @@
     PreferenceSetterResult.OK,
     PreferenceSetterResult.UNSUPPORTED,
     PreferenceSetterResult.DISABLED,
+    PreferenceSetterResult.RESTRICTED,
     PreferenceSetterResult.UNAVAILABLE,
+    PreferenceSetterResult.REQUIRE_APP_PERMISSION,
+    PreferenceSetterResult.REQUIRE_USER_AGREEMENT,
+    PreferenceSetterResult.DISALLOW,
     PreferenceSetterResult.INVALID_REQUEST,
     PreferenceSetterResult.INTERNAL_ERROR,
 )
@@ -87,14 +92,17 @@
 }
 
 /** Preference setter API implementation. */
-class PreferenceSetterApiHandler(override val id: Int) : ApiHandler<PreferenceSetterRequest, Int> {
+class PreferenceSetterApiHandler(
+    override val id: Int,
+    private val permissionChecker: ApiPermissionChecker<PreferenceSetterRequest>,
+) : ApiHandler<PreferenceSetterRequest, Int> {
 
     override fun hasPermission(
         application: Application,
         myUid: Int,
         callingUid: Int,
         request: PreferenceSetterRequest,
-    ): Boolean = true
+    ) = permissionChecker.hasPermission(application, myUid, callingUid, request)
 
     override suspend fun invoke(
         application: Application,
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
index d9b9590..1bda277 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
@@ -16,8 +16,10 @@
 
 package com.android.settingslib.graph
 
+import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.net.Uri
 import android.os.Bundle
 import com.android.settingslib.graph.proto.BundleProto
 import com.android.settingslib.graph.proto.BundleProto.BundleValue
@@ -42,6 +44,20 @@
     this@toProto.type?.let { mimeType = it }
 }
 
+fun IntentProto.toIntent(): Intent? {
+    if (!hasComponent()) return null
+    val componentName = ComponentName.unflattenFromString(component) ?: return null
+    val intent = Intent()
+    intent.component = componentName
+    if (hasAction()) intent.action = action
+    if (hasData()) intent.data = Uri.parse(data)
+    if (hasPkg()) intent.`package` = pkg
+    if (hasFlags()) intent.flags = flags
+    if (hasExtras()) intent.putExtras(extras.toBundle())
+    if (hasMimeType()) intent.setType(mimeType)
+    return intent
+}
+
 fun Bundle.toProto(): BundleProto = bundleProto {
     fun toProto(value: Any): BundleValue = bundleValueProto {
         when (value) {
@@ -61,14 +77,18 @@
     }
 }
 
-fun BundleValue.stringify(): String =
-    when {
-        hasBooleanValue() -> "$valueCase"
-        hasBytesValue() -> "$bytesValue"
-        hasIntValue() -> "$intValue"
-        hasLongValue() -> "$longValue"
-        hasStringValue() -> stringValue
-        hasDoubleValue() -> "$doubleValue"
-        hasBundleValue() -> "$bundleValue"
-        else -> "Unknown"
+fun BundleProto.toBundle(): Bundle =
+    Bundle().apply {
+        for ((key, value) in valuesMap) {
+            when {
+                value.hasBooleanValue() -> putBoolean(key, value.booleanValue)
+                value.hasBytesValue() -> putByteArray(key, value.bytesValue.toByteArray())
+                value.hasIntValue() -> putInt(key, value.intValue)
+                value.hasLongValue() -> putLong(key, value.longValue)
+                value.hasStringValue() -> putString(key, value.stringValue)
+                value.hasDoubleValue() -> putDouble(key, value.doubleValue)
+                value.hasBundleValue() -> putBundle(key, value.bundleValue.toBundle())
+                else -> throw IllegalArgumentException("Unknown type: ${value.javaClass} $value")
+            }
+        }
     }
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
index 802141d..4febd89 100644
--- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
@@ -56,6 +56,27 @@
     val responseCodec: MessageCodec<Response>
 }
 
+/** Permission checker for api. */
+fun interface ApiPermissionChecker<R> {
+    /**
+     * Returns if the request is permitted.
+     *
+     * @param application application context
+     * @param myUid uid of current process
+     * @param callingUid uid of peer process
+     * @param request API request
+     * @return `false` if permission is denied, otherwise `true`
+     */
+    fun hasPermission(application: Application, myUid: Int, callingUid: Int, request: R): Boolean
+
+    companion object {
+        private val ALWAYS_ALLOW = ApiPermissionChecker<Any> { _, _, _, _ -> true }
+
+        @Suppress("UNCHECKED_CAST")
+        fun <T> alwaysAllow(): ApiPermissionChecker<T> = ALWAYS_ALLOW as ApiPermissionChecker<T>
+    }
+}
+
 /**
  * Handler of API.
  *
@@ -64,18 +85,8 @@
  *
  * The implementation must be threadsafe.
  */
-interface ApiHandler<Request, Response> : ApiDescriptor<Request, Response> {
-    /**
-     * Returns if the request is permitted.
-     *
-     * @return `false` if permission is denied, otherwise `true`
-     */
-    fun hasPermission(
-        application: Application,
-        myUid: Int,
-        callingUid: Int,
-        request: Request,
-    ): Boolean
+interface ApiHandler<Request, Response> :
+    ApiDescriptor<Request, Response>, ApiPermissionChecker<Request> {
 
     /**
      * Invokes the API.
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
index 7ffefed..ef907e1 100644
--- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
@@ -320,6 +320,11 @@
             }
         }
 
+        override fun onNullBinding(name: ComponentName) {
+            Log.i(TAG, "onNullBinding $name")
+            close(ClientBindServiceException(null))
+        }
+
         internal open fun drainPendingRequests() {
             disposableHandle = null
             if (pendingRequests.isEmpty()) {
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
index 6e38df1..1823ba6 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
@@ -19,9 +19,14 @@
 import android.app.Application
 import com.android.settingslib.graph.GetPreferenceGraphApiHandler
 import com.android.settingslib.graph.GetPreferenceGraphRequest
+import com.android.settingslib.ipc.ApiPermissionChecker
+import com.android.settingslib.preference.PreferenceScreenProvider
 
 /** Api to get preference graph. */
-internal class PreferenceGraphApi : GetPreferenceGraphApiHandler() {
+internal class PreferenceGraphApi(
+    preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>>,
+    private val permissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>,
+) : GetPreferenceGraphApiHandler(preferenceScreenProviders) {
 
     override val id: Int
         get() = API_GET_PREFERENCE_GRAPH
@@ -31,5 +36,5 @@
         myUid: Int,
         callingUid: Int,
         request: GetPreferenceGraphRequest,
-    ) = true
+    ) = permissionChecker.hasPermission(application, myUid, callingUid, request)
 }
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
index 8ebb145..ed748bb 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
@@ -16,10 +16,14 @@
 
 package com.android.settingslib.service
 
+import com.android.settingslib.graph.GetPreferenceGraphRequest
 import com.android.settingslib.graph.PreferenceSetterApiHandler
+import com.android.settingslib.graph.PreferenceSetterRequest
 import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.ApiPermissionChecker
 import com.android.settingslib.ipc.MessengerService
 import com.android.settingslib.ipc.PermissionChecker
+import com.android.settingslib.preference.PreferenceScreenProvider
 
 /**
  * Preference service providing a bunch of APIs.
@@ -28,14 +32,21 @@
  * [PREFERENCE_SERVICE_ACTION].
  */
 open class PreferenceService(
-    permissionChecker: PermissionChecker,
     name: String = "PreferenceService",
+    permissionChecker: PermissionChecker = PermissionChecker { _, _, _ -> true },
+    preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>> = setOf(),
+    graphPermissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>? = null,
+    setterPermissionChecker: ApiPermissionChecker<PreferenceSetterRequest>? = null,
+    vararg apiHandlers: ApiHandler<*, *>,
 ) :
     MessengerService(
-        listOf<ApiHandler<*, *>>(
-            PreferenceGraphApi(),
-            PreferenceSetterApiHandler(API_PREFERENCE_SETTER),
-        ),
+        mutableListOf<ApiHandler<*, *>>().apply {
+            graphPermissionChecker?.let { add(PreferenceGraphApi(preferenceScreenProviders, it)) }
+            setterPermissionChecker?.let {
+                add(PreferenceSetterApiHandler(API_PREFERENCE_SETTER, it))
+            }
+            addAll(apiHandlers)
+        },
         permissionChecker,
         name,
     )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
index 1f38a66..7655daa 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -18,5 +18,14 @@
 
 const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
 
+/** API id for retrieving preference graph. */
 internal const val API_GET_PREFERENCE_GRAPH = 1
+
+/** API id for preference value setter. */
 internal const val API_PREFERENCE_SETTER = 2
+
+/**
+ * The max API id reserved for internal preference service usages. Custom API id should start with
+ * **1000** to avoid conflict.
+ */
+internal const val API_MAX_RESERVED = 999
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 83ee975..80e5e59 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -15,6 +15,7 @@
  */
 package com.android.settingslib.media;
 
+import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
 import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY;
@@ -103,7 +104,8 @@
                             TYPE_USB_DEVICE,
                             TYPE_USB_HEADSET,
                             TYPE_USB_ACCESSORY,
-                            TYPE_BLUETOOTH_SCO ->
+                            TYPE_BLUETOOTH_SCO,
+                            TYPE_BLE_HEADSET ->
                     true;
             default -> false;
         };
@@ -124,7 +126,7 @@
                     mProductName != null
                             ? mProductName
                             : mContext.getString(R.string.media_transfer_usb_device_mic_name);
-            case TYPE_BLUETOOTH_SCO ->
+            case TYPE_BLUETOOTH_SCO, TYPE_BLE_HEADSET ->
                     mProductName != null
                             ? mProductName
                             : mContext.getString(R.string.media_transfer_bt_device_mic_name);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
index 7775b91..8624c4d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -38,6 +38,7 @@
     private final int WIRED_HEADSET_ID = 2;
     private final int USB_HEADSET_ID = 3;
     private final int BT_HEADSET_ID = 4;
+    private final int BLE_HEADSET_ID = 5;
     private final int MAX_VOLUME = 1;
     private final int CURRENT_VOLUME = 0;
     private final boolean IS_VOLUME_FIXED = true;
@@ -45,6 +46,7 @@
     private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
     private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
     private static final String PRODUCT_NAME_BT_HEADSET = "My Bluetooth Headset";
+    private static final String PRODUCT_NAME_BLE_HEADSET = "My BLE Headset";
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
@@ -163,4 +165,35 @@
         assertThat(btMediaDevice.getName())
                 .isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name));
     }
+
+    @Test
+    public void getName_returnCorrectName_bleHeadset() {
+        InputMediaDevice bleMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(BLE_HEADSET_ID),
+                        AudioDeviceInfo.TYPE_BLE_HEADSET,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED,
+                        PRODUCT_NAME_BLE_HEADSET);
+        assertThat(bleMediaDevice).isNotNull();
+        assertThat(bleMediaDevice.getName()).isEqualTo(PRODUCT_NAME_BLE_HEADSET);
+    }
+
+    @Test
+    public void getName_returnCorrectName_bleHeadset_nullProductName() {
+        InputMediaDevice bleMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(BLE_HEADSET_ID),
+                        AudioDeviceInfo.TYPE_BLE_HEADSET,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED,
+                        null);
+        assertThat(bleMediaDevice).isNotNull();
+        assertThat(bleMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name));
+    }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 7b6321d..526320d 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -369,6 +369,9 @@
     <!-- Permission needed for CTS test - UnsupportedErrorDialogTests -->
     <uses-permission android:name="android.permission.RESET_APP_ERRORS" />
 
+    <!-- Permission needed tests -->
+    <uses-permission android:name="android.permission.OBSERVE_PICTURE_PROFILES" />
+
     <!-- Permission needed for CTS test - CtsSystemUiTestCases:PipNotificationTests -->
     <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
 
@@ -953,6 +956,13 @@
     <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"
+        android:featureFlag="android.security.afl_api"/>
+    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+
     <!-- Permission required for CTS test - CtsAppTestCases -->
     <uses-permission android:name="android.permission.KILL_UID" />
 
diff --git a/packages/Shell/aconfig/Android.bp b/packages/Shell/aconfig/Android.bp
new file mode 100644
index 0000000..1d797b2
--- /dev/null
+++ b/packages/Shell/aconfig/Android.bp
@@ -0,0 +1,13 @@
+aconfig_declarations {
+    name: "wear_aconfig_declarations",
+    package: "com.android.shell.flags",
+    container: "system",
+    srcs: [
+        "wear.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "wear_aconfig_declarations_flags_java_lib",
+    aconfig_declarations: "wear_aconfig_declarations",
+}
diff --git a/packages/Shell/aconfig/wear.aconfig b/packages/Shell/aconfig/wear.aconfig
new file mode 100644
index 0000000..e07bd96
--- /dev/null
+++ b/packages/Shell/aconfig/wear.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.shell.flags"
+container: "system"
+
+flag {
+    name: "handle_bugreports_for_wear"
+    namespace: "wear_services"
+    description: "This flag enables Shell to propagate bugreport results to WearServices."
+    bug: "378060870"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 18f40c9..eee0caf 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -24,18 +24,22 @@
 import android.content.ComponentName
 import android.graphics.Color
 import android.graphics.Matrix
+import android.graphics.PointF
 import android.graphics.Rect
 import android.graphics.RectF
 import android.os.Binder
 import android.os.Build
 import android.os.Handler
+import android.os.IBinder
 import android.os.Looper
 import android.os.RemoteException
+import android.util.ArrayMap
 import android.util.Log
 import android.view.IRemoteAnimationFinishedCallback
 import android.view.IRemoteAnimationRunner
 import android.view.RemoteAnimationAdapter
 import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
 import android.view.SyncRtSurfaceTransactionApplier
 import android.view.View
 import android.view.ViewGroup
@@ -45,8 +49,12 @@
 import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.view.animation.PathInterpolator
+import android.window.IRemoteTransition
+import android.window.IRemoteTransitionFinishedCallback
 import android.window.RemoteTransition
 import android.window.TransitionFilter
+import android.window.TransitionInfo
+import android.window.WindowAnimationState
 import androidx.annotation.AnyThread
 import androidx.annotation.BinderThread
 import androidx.annotation.UiThread
@@ -55,11 +63,14 @@
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.systemui.Flags.activityTransitionUseLargestWindow
 import com.android.systemui.Flags.translucentOccludingActivityFix
+import com.android.systemui.animation.TransitionAnimator.Companion.assertLongLivedReturnAnimations
+import com.android.systemui.animation.TransitionAnimator.Companion.assertReturnAnimations
+import com.android.systemui.animation.TransitionAnimator.Companion.longLivedReturnAnimationsEnabled
+import com.android.systemui.animation.TransitionAnimator.Companion.returnAnimationsEnabled
 import com.android.systemui.animation.TransitionAnimator.Companion.toTransitionState
-import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
-import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived
 import com.android.wm.shell.shared.IShellTransitions
 import com.android.wm.shell.shared.ShellTransitions
+import com.android.wm.shell.shared.TransitionUtil
 import java.util.concurrent.Executor
 import kotlin.math.roundToInt
 
@@ -194,7 +205,13 @@
         private const val LONG_TRANSITION_TIMEOUT = 5_000L
 
         private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator {
-            return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS)
+            return TransitionAnimator(
+                mainExecutor,
+                TIMINGS,
+                INTERPOLATORS,
+                SPRING_TIMINGS,
+                SPRING_INTERPOLATORS,
+            )
         }
 
         private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator {
@@ -275,7 +292,7 @@
                     "ActivityTransitionAnimator.callback must be set before using this animator"
                 )
         val runner = createRunner(controller)
-        val runnerDelegate = runner.delegate!!
+        val runnerDelegate = runner.delegate
         val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
 
         // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
@@ -330,7 +347,11 @@
         // If we expect an animation, post a timeout to cancel it in case the remote animation is
         // never started.
         if (willAnimate) {
-            runnerDelegate.postTimeouts()
+            if (longLivedReturnAnimationsEnabled()) {
+                runner.postTimeouts()
+            } else {
+                runnerDelegate!!.postTimeouts()
+            }
 
             // Hide the keyguard using the launch animation instead of the default unlock animation.
             if (hideKeyguardWithAnimation) {
@@ -390,7 +411,7 @@
         launchController: Controller,
         transitionRegister: TransitionRegister?,
     ) {
-        if (!returnAnimationFrameworkLibrary()) return
+        if (!returnAnimationsEnabled()) return
 
         var cleanUpRunnable: Runnable? = null
         val returnRunner =
@@ -413,7 +434,8 @@
                     private fun cleanUp() {
                         cleanUpRunnable?.run()
                     }
-                }
+                },
+                initializeLazily = longLivedReturnAnimationsEnabled(),
             )
 
         // mTypeSet and mModes match back signals only, and not home. This is on purpose, because
@@ -435,7 +457,11 @@
                 "${launchController.transitionCookie}_returnTransition",
             )
 
-        transitionRegister?.register(filter, transition)
+        transitionRegister?.register(
+            filter,
+            transition,
+            includeTakeover = longLivedReturnAnimationsEnabled(),
+        )
         cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) }
     }
 
@@ -451,7 +477,10 @@
 
     /** Create a new animation [Runner] controlled by [controller]. */
     @VisibleForTesting
-    fun createRunner(controller: Controller): Runner {
+    @JvmOverloads
+    fun createRunner(controller: Controller, initializeLazily: Boolean = false): Runner {
+        if (initializeLazily) assertLongLivedReturnAnimations()
+
         // Make sure we use the modified timings when animating a dialog into an app.
         val transitionAnimator =
             if (controller.isDialogLaunch) {
@@ -460,7 +489,13 @@
                 transitionAnimator
             }
 
-        return Runner(controller, callback!!, transitionAnimator, lifecycleListener)
+        return Runner(
+            controller,
+            callback!!,
+            transitionAnimator,
+            lifecycleListener,
+            initializeLazily,
+        )
     }
 
     interface PendingIntentStarter {
@@ -628,10 +663,7 @@
      * this registration.
      */
     fun register(controller: Controller) {
-        check(returnAnimationFrameworkLongLived()) {
-            "Long-lived registrations cannot be used when the returnAnimationFrameworkLongLived " +
-                "flag is disabled"
-        }
+        assertLongLivedReturnAnimations()
 
         if (transitionRegister == null) {
             throw IllegalStateException(
@@ -667,10 +699,10 @@
             }
         val launchRemoteTransition =
             RemoteTransition(
-                RemoteAnimationRunnerCompat.wrap(createRunner(controller)),
+                OriginTransition(createRunner(controller, initializeLazily = true)),
                 "${cookie}_launchTransition",
             )
-        transitionRegister.register(launchFilter, launchRemoteTransition)
+        transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true)
 
         val returnController =
             object : Controller by controller {
@@ -689,10 +721,10 @@
             }
         val returnRemoteTransition =
             RemoteTransition(
-                RemoteAnimationRunnerCompat.wrap(createRunner(returnController)),
+                OriginTransition(createRunner(returnController, initializeLazily = true)),
                 "${cookie}_returnTransition",
             )
-        transitionRegister.register(returnFilter, returnRemoteTransition)
+        transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true)
 
         longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition)
     }
@@ -738,14 +770,154 @@
         }
     }
 
+    /** [Runner] wrapper that supports animation takeovers. */
+    private inner class OriginTransition(private val runner: Runner) : IRemoteTransition {
+        private val delegate = RemoteAnimationRunnerCompat.wrap(runner)
+
+        init {
+            assertLongLivedReturnAnimations()
+        }
+
+        override fun startAnimation(
+            token: IBinder?,
+            info: TransitionInfo?,
+            t: SurfaceControl.Transaction?,
+            finishCallback: IRemoteTransitionFinishedCallback?,
+        ) {
+            delegate.startAnimation(token, info, t, finishCallback)
+        }
+
+        override fun mergeAnimation(
+            transition: IBinder?,
+            info: TransitionInfo?,
+            t: SurfaceControl.Transaction?,
+            mergeTarget: IBinder?,
+            finishCallback: IRemoteTransitionFinishedCallback?,
+        ) {
+            delegate.mergeAnimation(transition, info, t, mergeTarget, finishCallback)
+        }
+
+        override fun onTransitionConsumed(transition: IBinder?, aborted: Boolean) {
+            delegate.onTransitionConsumed(transition, aborted)
+        }
+
+        override fun takeOverAnimation(
+            token: IBinder?,
+            info: TransitionInfo?,
+            t: SurfaceControl.Transaction?,
+            finishCallback: IRemoteTransitionFinishedCallback?,
+            states: Array<WindowAnimationState>,
+        ) {
+            if (info == null || t == null) {
+                Log.e(
+                    TAG,
+                    "Skipping the animation takeover because the required data is missing: " +
+                        "info=$info, transaction=$t",
+                )
+                return
+            }
+
+            // The following code converts the contents of the given TransitionInfo into
+            // RemoteAnimationTargets. This is necessary because we must currently support both the
+            // new (Shell, remote transitions) and old (remote animations) framework to maintain
+            // functionality for all users of the library.
+            val apps = ArrayList<RemoteAnimationTarget>()
+            val filteredStates = ArrayList<WindowAnimationState>()
+            val leashMap = ArrayMap<SurfaceControl, SurfaceControl>()
+            val leafTaskFilter = TransitionUtil.LeafTaskFilter()
+
+            // About layering: we divide up the "layer space" into 2 regions (each the size of the
+            // change count). This lets us categorize things into above and below while
+            // maintaining their relative ordering.
+            val belowLayers = info.changes.size
+            val aboveLayers = info.changes.size * 2
+            for (i in info.changes.indices) {
+                val change = info.changes[i]
+                if (change == null || change.taskInfo == null) {
+                    continue
+                }
+
+                val taskInfo = change.taskInfo
+
+                if (TransitionUtil.isWallpaper(change)) {
+                    val target =
+                        TransitionUtil.newTarget(
+                            change,
+                            belowLayers - i, // wallpapers go into the "below" layer space
+                            info,
+                            t,
+                            leashMap,
+                        )
+
+                    // Make all the wallpapers opaque.
+                    t.setAlpha(target.leash, 1f)
+                } else if (leafTaskFilter.test(change)) {
+                    // Start by putting everything into the "below" layer space.
+                    val target =
+                        TransitionUtil.newTarget(change, belowLayers - i, info, t, leashMap)
+                    apps.add(target)
+                    filteredStates.add(states[i])
+
+                    // Make all the apps opaque.
+                    t.setAlpha(target.leash, 1f)
+
+                    if (
+                        TransitionUtil.isClosingType(change.mode) &&
+                            taskInfo?.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME
+                    ) {
+                        // Raise closing task to "above" layer so it isn't covered.
+                        t.setLayer(target.leash, aboveLayers - i)
+                    }
+                } else if (TransitionInfo.isIndependent(change, info)) {
+                    // Root tasks
+                    if (TransitionUtil.isClosingType(change.mode)) {
+                        // Raise closing task to "above" layer so it isn't covered.
+                        t.setLayer(change.leash, aboveLayers - i)
+                    } else if (TransitionUtil.isOpeningType(change.mode)) {
+                        // Put into the "below" layer space.
+                        t.setLayer(change.leash, belowLayers - i)
+                    }
+                } else if (TransitionUtil.isDividerBar(change)) {
+                    val target =
+                        TransitionUtil.newTarget(change, belowLayers - i, info, t, leashMap)
+                    apps.add(target)
+                    filteredStates.add(states[i])
+                }
+            }
+
+            val wrappedCallback: IRemoteAnimationFinishedCallback =
+                object : IRemoteAnimationFinishedCallback.Stub() {
+                    override fun onAnimationFinished() {
+                        leashMap.clear()
+                        val finishTransaction = SurfaceControl.Transaction()
+                        finishCallback?.onTransitionFinished(null, finishTransaction)
+                        finishTransaction.close()
+                    }
+                }
+
+            runner.takeOverAnimation(
+                apps.toTypedArray(),
+                filteredStates.toTypedArray(),
+                t,
+                wrappedCallback,
+            )
+        }
+
+        override fun asBinder(): IBinder {
+            return delegate.asBinder()
+        }
+    }
+
     @VisibleForTesting
     inner class Runner(
-        controller: Controller,
-        callback: Callback,
+        private val controller: Controller,
+        private val callback: Callback,
         /** The animator to use to animate the window transition. */
-        transitionAnimator: TransitionAnimator,
+        private val transitionAnimator: TransitionAnimator,
         /** Listener for animation lifecycle events. */
-        listener: Listener? = null,
+        private val listener: Listener? = null,
+        /** Whether the internal [delegate] should be initialized lazily. */
+        private val initializeLazily: Boolean = false,
     ) : IRemoteAnimationRunner.Stub() {
         // This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
         // etc.) are possible. So we need to make sure we drop any references that might
@@ -753,15 +925,14 @@
         @VisibleForTesting var delegate: AnimationDelegate?
 
         init {
-            delegate =
-                AnimationDelegate(
-                    mainExecutor,
-                    controller,
-                    callback,
-                    DelegatingAnimationCompletionListener(listener, this::dispose),
-                    transitionAnimator,
-                    disableWmTimeout,
-                )
+            delegate = null
+            if (!initializeLazily) {
+                // Ephemeral launches bundle the runner with the launch request (instead of being
+                // registered ahead of time for later use). This means that there could be a timeout
+                // between creation and invocation, so the delegate needs to exist from the
+                // beginning in order to handle such timeout.
+                createDelegate()
+            }
         }
 
         @BinderThread
@@ -772,6 +943,36 @@
             nonApps: Array<out RemoteAnimationTarget>?,
             finishedCallback: IRemoteAnimationFinishedCallback?,
         ) {
+            initAndRun(finishedCallback) { delegate ->
+                delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback)
+            }
+        }
+
+        @VisibleForTesting
+        @BinderThread
+        fun takeOverAnimation(
+            apps: Array<RemoteAnimationTarget>?,
+            windowAnimationStates: Array<WindowAnimationState>,
+            startTransaction: SurfaceControl.Transaction,
+            finishedCallback: IRemoteAnimationFinishedCallback?,
+        ) {
+            assertLongLivedReturnAnimations()
+            initAndRun(finishedCallback) { delegate ->
+                delegate.takeOverAnimation(
+                    apps,
+                    windowAnimationStates,
+                    startTransaction,
+                    finishedCallback,
+                )
+            }
+        }
+
+        @BinderThread
+        private fun initAndRun(
+            finishedCallback: IRemoteAnimationFinishedCallback?,
+            performAnimation: (AnimationDelegate) -> Unit,
+        ) {
+            maybeSetUp()
             val delegate = delegate
             mainExecutor.execute {
                 if (delegate == null) {
@@ -780,7 +981,7 @@
                     // signal back that we're done with it.
                     finishedCallback?.onAnimationFinished()
                 } else {
-                    delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback)
+                    performAnimation(delegate)
                 }
             }
         }
@@ -794,6 +995,32 @@
             }
         }
 
+        @VisibleForTesting
+        @UiThread
+        fun postTimeouts() {
+            maybeSetUp()
+            delegate?.postTimeouts()
+        }
+
+        @AnyThread
+        private fun maybeSetUp() {
+            if (!initializeLazily || delegate != null) return
+            createDelegate()
+        }
+
+        @AnyThread
+        private fun createDelegate() {
+            delegate =
+                AnimationDelegate(
+                    mainExecutor,
+                    controller,
+                    callback,
+                    DelegatingAnimationCompletionListener(listener, this::dispose),
+                    transitionAnimator,
+                    disableWmTimeout,
+                )
+        }
+
         @AnyThread
         fun dispose() {
             // Drop references to animation controller once we're done with the animation
@@ -867,7 +1094,7 @@
         init {
             // We do this check here to cover all entry points, including Launcher which doesn't
             // call startIntentWithAnimation()
-            if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag()
+            if (!controller.isLaunching) assertReturnAnimations()
         }
 
         @UiThread
@@ -893,19 +1120,73 @@
             nonApps: Array<out RemoteAnimationTarget>?,
             callback: IRemoteAnimationFinishedCallback?,
         ) {
+            val window = setUpAnimation(apps, callback) ?: return
+
+            if (controller.windowAnimatorState == null || !longLivedReturnAnimationsEnabled()) {
+                val navigationBar =
+                    nonApps?.firstOrNull {
+                        it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
+                    }
+
+                startAnimation(window, navigationBar, iCallback = callback)
+            } else {
+                // If a [controller.windowAnimatorState] exists, treat this like a takeover.
+                takeOverAnimationInternal(
+                    window,
+                    startWindowStates = null,
+                    startTransaction = null,
+                    callback,
+                )
+            }
+        }
+
+        @UiThread
+        internal fun takeOverAnimation(
+            apps: Array<out RemoteAnimationTarget>?,
+            startWindowStates: Array<WindowAnimationState>,
+            startTransaction: SurfaceControl.Transaction,
+            callback: IRemoteAnimationFinishedCallback?,
+        ) {
+            val window = setUpAnimation(apps, callback) ?: return
+            takeOverAnimationInternal(window, startWindowStates, startTransaction, callback)
+        }
+
+        private fun takeOverAnimationInternal(
+            window: RemoteAnimationTarget,
+            startWindowStates: Array<WindowAnimationState>?,
+            startTransaction: SurfaceControl.Transaction?,
+            callback: IRemoteAnimationFinishedCallback?,
+        ) {
+            val useSpring =
+                !controller.isLaunching && startWindowStates != null && startTransaction != null
+            startAnimation(
+                window,
+                navigationBar = null,
+                useSpring,
+                startWindowStates,
+                startTransaction,
+                callback,
+            )
+        }
+
+        @UiThread
+        private fun setUpAnimation(
+            apps: Array<out RemoteAnimationTarget>?,
+            callback: IRemoteAnimationFinishedCallback?,
+        ): RemoteAnimationTarget? {
             removeTimeouts()
 
             // The animation was started too late and we already notified the controller that it
             // timed out.
             if (timedOut) {
                 callback?.invoke()
-                return
+                return null
             }
 
             // This should not happen, but let's make sure we don't start the animation if it was
             // cancelled before and we already notified the controller.
             if (cancelled) {
-                return
+                return null
             }
 
             val window = findTargetWindowIfPossible(apps)
@@ -921,15 +1202,10 @@
                 }
                 controller.onTransitionAnimationCancelled()
                 listener?.onTransitionAnimationCancelled()
-                return
+                return null
             }
 
-            val navigationBar =
-                nonApps?.firstOrNull {
-                    it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
-                }
-
-            startAnimation(window, navigationBar, callback)
+            return window
         }
 
         private fun findTargetWindowIfPossible(
@@ -950,7 +1226,7 @@
             for (it in apps) {
                 if (it.mode == targetMode) {
                     if (activityTransitionUseLargestWindow()) {
-                        if (returnAnimationFrameworkLibrary()) {
+                        if (returnAnimationsEnabled()) {
                             // If the controller contains a cookie, _only_ match if either the
                             // candidate contains the matching cookie, or a component is also
                             // defined and is a match.
@@ -995,8 +1271,11 @@
 
         private fun startAnimation(
             window: RemoteAnimationTarget,
-            navigationBar: RemoteAnimationTarget?,
-            iCallback: IRemoteAnimationFinishedCallback?,
+            navigationBar: RemoteAnimationTarget? = null,
+            useSpring: Boolean = false,
+            startingWindowStates: Array<WindowAnimationState>? = null,
+            startTransaction: SurfaceControl.Transaction? = null,
+            iCallback: IRemoteAnimationFinishedCallback? = null,
         ) {
             if (TransitionAnimator.DEBUG) {
                 Log.d(TAG, "Remote animation started")
@@ -1046,18 +1325,66 @@
             val controller =
                 object : Controller by delegate {
                     override fun createAnimatorState(): TransitionAnimator.State {
-                        if (isLaunching) return delegate.createAnimatorState()
-                        return delegate.windowAnimatorState?.toTransitionState()
-                            ?: getWindowRadius(isExpandingFullyAbove).let {
-                                TransitionAnimator.State(
-                                    top = windowBounds.top,
-                                    bottom = windowBounds.bottom,
-                                    left = windowBounds.left,
-                                    right = windowBounds.right,
-                                    topCornerRadius = it,
-                                    bottomCornerRadius = it,
-                                )
+                        if (isLaunching) {
+                            return delegate.createAnimatorState()
+                        } else if (!longLivedReturnAnimationsEnabled()) {
+                            return delegate.windowAnimatorState?.toTransitionState()
+                                ?: getWindowRadius(isExpandingFullyAbove).let {
+                                    TransitionAnimator.State(
+                                        top = windowBounds.top,
+                                        bottom = windowBounds.bottom,
+                                        left = windowBounds.left,
+                                        right = windowBounds.right,
+                                        topCornerRadius = it,
+                                        bottomCornerRadius = it,
+                                    )
+                                }
+                        }
+
+                        // The states are sorted matching the changes inside the transition info.
+                        // Using this info, the RemoteAnimationTargets are created, with their
+                        // prefixOrderIndex fields in reverse order to that of changes. To extract
+                        // the right state, we need to invert again.
+                        val windowState =
+                            if (startingWindowStates != null) {
+                                startingWindowStates[
+                                    startingWindowStates.size - window.prefixOrderIndex]
+                            } else {
+                                controller.windowAnimatorState
                             }
+
+                        // TODO(b/323863002): use the timestamp and velocity to update the initial
+                        //   position.
+                        val bounds = windowState?.bounds
+                        val left: Int = bounds?.left?.toInt() ?: windowBounds.left
+                        val top: Int = bounds?.top?.toInt() ?: windowBounds.top
+                        val right: Int = bounds?.right?.toInt() ?: windowBounds.right
+                        val bottom: Int = bounds?.bottom?.toInt() ?: windowBounds.bottom
+
+                        val width = windowBounds.right - windowBounds.left
+                        val height = windowBounds.bottom - windowBounds.top
+                        // Scale the window. We use the max of (widthRatio, heightRatio) so that
+                        // there is no blank space on any side.
+                        val widthRatio = (right - left).toFloat() / width
+                        val heightRatio = (bottom - top).toFloat() / height
+                        val startScale = maxOf(widthRatio, heightRatio)
+
+                        val maybeRadius = windowState?.topLeftRadius
+                        val windowRadius =
+                            if (maybeRadius != null) {
+                                maybeRadius * startScale
+                            } else {
+                                getWindowRadius(isExpandingFullyAbove)
+                            }
+
+                        return TransitionAnimator.State(
+                            top = top,
+                            bottom = bottom,
+                            left = left,
+                            right = right,
+                            topCornerRadius = windowRadius,
+                            bottomCornerRadius = windowRadius,
+                        )
                     }
 
                     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -1071,6 +1398,19 @@
                                     "[controller=$delegate]",
                             )
                         }
+
+                        if (startTransaction != null) {
+                            // Calling applyStateToWindow() here avoids skipping a frame when taking
+                            // over an animation.
+                            applyStateToWindow(
+                                window,
+                                createAnimatorState(),
+                                linearProgress = 0f,
+                                useSpring,
+                                startTransaction,
+                            )
+                        }
+
                         delegate.onTransitionAnimationStart(isExpandingFullyAbove)
                     }
 
@@ -1094,14 +1434,29 @@
                         progress: Float,
                         linearProgress: Float,
                     ) {
-                        applyStateToWindow(window, state, linearProgress)
+                        applyStateToWindow(window, state, linearProgress, useSpring)
                         navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
 
                         listener?.onTransitionAnimationProgress(linearProgress)
                         delegate.onTransitionAnimationProgress(state, progress, linearProgress)
                     }
                 }
-
+            val windowState =
+                if (startingWindowStates != null) {
+                    startingWindowStates[startingWindowStates.size - window.prefixOrderIndex]
+                } else {
+                    controller.windowAnimatorState
+                }
+            val velocityPxPerS =
+                if (longLivedReturnAnimationsEnabled() && windowState?.velocityPxPerMs != null) {
+                    val xVelocityPxPerS = windowState.velocityPxPerMs.x * 1000
+                    val yVelocityPxPerS = windowState.velocityPxPerMs.y * 1000
+                    PointF(xVelocityPxPerS, yVelocityPxPerS)
+                } else if (useSpring) {
+                    PointF(0f, 0f)
+                } else {
+                    null
+                }
             animation =
                 transitionAnimator.startAnimation(
                     controller,
@@ -1109,6 +1464,7 @@
                     windowBackgroundColor,
                     fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
                     drawHole = !controller.isBelowAnimatingWindow,
+                    startVelocity = velocityPxPerS,
                 )
         }
 
@@ -1128,6 +1484,8 @@
             window: RemoteAnimationTarget,
             state: TransitionAnimator.State,
             linearProgress: Float,
+            useSpring: Boolean,
+            transaction: SurfaceControl.Transaction? = null,
         ) {
             if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) {
                 // Don't apply any transaction if the view root we synchronize with was detached or
@@ -1171,25 +1529,47 @@
                 windowCropF.bottom.roundToInt(),
             )
 
-            val windowAnimationDelay =
+            val interpolators: TransitionAnimator.Interpolators
+            val windowProgress: Float
+
+            if (useSpring) {
+                val windowAnimationDelay: Float
+                val windowAnimationDuration: Float
                 if (controller.isLaunching) {
-                    TIMINGS.contentAfterFadeInDelay
+                    windowAnimationDelay = SPRING_TIMINGS.contentAfterFadeInDelay
+                    windowAnimationDuration = SPRING_TIMINGS.contentAfterFadeInDuration
                 } else {
-                    TIMINGS.contentBeforeFadeOutDelay
+                    windowAnimationDelay = SPRING_TIMINGS.contentBeforeFadeOutDelay
+                    windowAnimationDuration = SPRING_TIMINGS.contentBeforeFadeOutDuration
                 }
-            val windowAnimationDuration =
+
+                interpolators = SPRING_INTERPOLATORS
+                windowProgress =
+                    TransitionAnimator.getProgress(
+                        linearProgress,
+                        windowAnimationDelay,
+                        windowAnimationDuration,
+                    )
+            } else {
+                val windowAnimationDelay: Long
+                val windowAnimationDuration: Long
                 if (controller.isLaunching) {
-                    TIMINGS.contentAfterFadeInDuration
+                    windowAnimationDelay = TIMINGS.contentAfterFadeInDelay
+                    windowAnimationDuration = TIMINGS.contentAfterFadeInDuration
                 } else {
-                    TIMINGS.contentBeforeFadeOutDuration
+                    windowAnimationDelay = TIMINGS.contentBeforeFadeOutDelay
+                    windowAnimationDuration = TIMINGS.contentBeforeFadeOutDuration
                 }
-            val windowProgress =
-                TransitionAnimator.getProgress(
-                    TIMINGS,
-                    linearProgress,
-                    windowAnimationDelay,
-                    windowAnimationDuration,
-                )
+
+                interpolators = INTERPOLATORS
+                windowProgress =
+                    TransitionAnimator.getProgress(
+                        TIMINGS,
+                        linearProgress,
+                        windowAnimationDelay,
+                        windowAnimationDuration,
+                    )
+            }
 
             // The alpha of the opening window. If it opens above the expandable, then it should
             // fade in progressively. Otherwise, it should be fully opaque and will be progressively
@@ -1197,12 +1577,12 @@
             val alpha =
                 if (controller.isBelowAnimatingWindow) {
                     if (controller.isLaunching) {
-                        INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(
+                        interpolators.contentAfterFadeInInterpolator.getInterpolation(
                             windowProgress
                         )
                     } else {
                         1 -
-                            INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation(
+                            interpolators.contentBeforeFadeOutInterpolator.getInterpolation(
                                 windowProgress
                             )
                     }
@@ -1216,6 +1596,7 @@
             // especially important for lock screen animations, where the window is not clipped by
             // the shade.
             val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
+
             val params =
                 SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
                     .withAlpha(alpha)
@@ -1223,11 +1604,15 @@
                     .withWindowCrop(windowCrop)
                     .withCornerRadius(cornerRadius)
                     .withVisibility(true)
-                    .build()
+            if (transaction != null) params.withMergeTransaction(transaction)
 
-            transactionApplier.scheduleApply(params)
+            transactionApplier.scheduleApply(params.build())
         }
 
+        // TODO(b/377643129): remote transitions have no way of identifying the navbar when
+        //  converting to RemoteAnimationTargets (and in my testing it was never included in the
+        //  transition at all). So this method is not used anymore. Remove or adapt once we fully
+        //  convert to remote transitions.
         private fun applyStateToNavigationBar(
             navigationBar: RemoteAnimationTarget,
             state: TransitionAnimator.State,
@@ -1362,9 +1747,17 @@
         }
 
         /** Register [remoteTransition] with WM Shell using the given [filter]. */
-        internal fun register(filter: TransitionFilter, remoteTransition: RemoteTransition) {
+        internal fun register(
+            filter: TransitionFilter,
+            remoteTransition: RemoteTransition,
+            includeTakeover: Boolean,
+        ) {
             shellTransitions?.registerRemote(filter, remoteTransition)
             iShellTransitions?.registerRemote(filter, remoteTransition)
+            if (includeTakeover) {
+                shellTransitions?.registerRemoteForTakeover(filter, remoteTransition)
+                iShellTransitions?.registerRemoteForTakeover(filter, remoteTransition)
+            }
         }
 
         /** Unregister [remoteTransition] from WM Shell. */
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 fdb4871..de4bdbc 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -38,6 +38,7 @@
 import com.android.internal.dynamicanimation.animation.SpringAnimation
 import com.android.internal.dynamicanimation.animation.SpringForce
 import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
+import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived
 import java.util.concurrent.Executor
 import kotlin.math.abs
 import kotlin.math.max
@@ -113,13 +114,26 @@
             )
         }
 
-        internal fun checkReturnAnimationFrameworkFlag() {
-            check(returnAnimationFrameworkLibrary()) {
-                "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " +
-                    "disabled"
+        internal fun assertReturnAnimations() {
+            check(returnAnimationsEnabled()) {
+                "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag " +
+                    "is disabled"
             }
         }
 
+        internal fun returnAnimationsEnabled() = returnAnimationFrameworkLibrary()
+
+        internal fun assertLongLivedReturnAnimations() {
+            check(longLivedReturnAnimationsEnabled()) {
+                "Long-lived registrations cannot be used when the " +
+                    "returnAnimationFrameworkLibrary or the " +
+                    "returnAnimationFrameworkLongLived flag are disabled"
+            }
+        }
+
+        internal fun longLivedReturnAnimationsEnabled() =
+            returnAnimationFrameworkLibrary() && returnAnimationFrameworkLongLived()
+
         internal fun WindowAnimationState.toTransitionState() =
             State().also {
                 bounds?.let { b ->
@@ -467,7 +481,8 @@
         drawHole: Boolean = false,
         startVelocity: PointF? = null,
     ): Animation {
-        if (!controller.isLaunching || startVelocity != null) checkReturnAnimationFrameworkFlag()
+        if (!controller.isLaunching) assertReturnAnimations()
+        if (startVelocity != null) assertLongLivedReturnAnimations()
 
         // We add an extra layer with the same color as the dialog/app splash screen background
         // color, which is usually the same color of the app background. We first fade in this layer
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index d546a5d..9de7a5d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -16,6 +16,7 @@
 import com.android.systemui.scene.ui.composable.transitions.communalToBouncerTransition
 import com.android.systemui.scene.ui.composable.transitions.communalToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.dreamToBouncerTransition
+import com.android.systemui.scene.ui.composable.transitions.dreamToCommunalTransition
 import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition
 import com.android.systemui.scene.ui.composable.transitions.dreamToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
@@ -58,6 +59,7 @@
 
     from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
     from(Scenes.Dream, to = Scenes.Bouncer) { dreamToBouncerTransition() }
+    from(Scenes.Dream, to = Scenes.Communal) { dreamToCommunalTransition() }
     from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() }
     from(Scenes.Dream, to = Scenes.Shade) { dreamToShadeTransition() }
     from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
new file mode 100644
index 0000000..93c10b6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.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.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.communal.ui.compose.AllElements
+import com.android.systemui.communal.ui.compose.Communal
+
+fun TransitionBuilder.dreamToCommunalTransition() {
+    spec = tween(durationMillis = 1000)
+
+    // Translate communal hub grid from the end direction.
+    translate(Communal.Elements.Grid, Edge.End)
+
+    // Fade all communal hub elements.
+    timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
index 71fa6c9..ce7a85b1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
@@ -19,11 +19,8 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
 import com.android.systemui.media.controls.ui.composable.MediaCarousel
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
@@ -31,24 +28,17 @@
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.goneToSplitShadeTransition(
-    durationScale: Double = 1.0,
-) {
+fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
     swipeSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance =
-        object : UserActionDistance {
-            override fun UserActionDistanceScope.absoluteDistance(
-                fromSceneSize: IntSize,
-                orientation: Orientation,
-            ): Float {
-                return fromSceneSize.height.toFloat() * 2 / 3f
-            }
-        }
+    distance = UserActionDistance { fromContent, _, _ ->
+        val fromContentSize = checkNotNull(fromContent.targetSize())
+        fromContentSize.height.toFloat() * 2 / 3f
+    }
 
     fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
index 1486ea7..1f7a738 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
@@ -19,35 +19,25 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.shade.ui.composable.Shade
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.lockscreenToSplitShadeTransition(
-    durationScale: Double = 1.0,
-) {
+fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
     swipeSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance =
-        object : UserActionDistance {
-            override fun UserActionDistanceScope.absoluteDistance(
-                fromSceneSize: IntSize,
-                orientation: Orientation,
-            ): Float {
-                return fromSceneSize.height.toFloat() * 2 / 3f
-            }
-        }
+    distance = UserActionDistance { fromContent, _, _ ->
+        val fromContentSize = checkNotNull(fromContent.targetSize())
+        fromContentSize.height.toFloat() * 2 / 3f
+    }
 
     fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
index 8cd357e..ba1972f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -1,46 +1,36 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.shadeToQuickSettingsTransition(
-    durationScale: Double = 1.0,
-) {
+fun TransitionBuilder.shadeToQuickSettingsTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    distance =
-        object : UserActionDistance {
-            override fun UserActionDistanceScope.absoluteDistance(
-                fromSceneSize: IntSize,
-                orientation: Orientation,
-            ): Float {
-                val distance =
-                    Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y
-                        ?: return 0f
-                return fromSceneSize.height - distance
-            }
-        }
+    distance = UserActionDistance { fromContent, _, _ ->
+        val distance =
+            Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y
+                ?: return@UserActionDistance 0f
+        val fromContentSize = checkNotNull(fromContent.targetSize())
+        fromContentSize.height - distance
+    }
 
     translate(Notifications.Elements.NotificationScrim, Edge.Bottom)
     timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
 
     translate(
         ShadeHeader.Elements.CollapsedContentStart,
-        y = ShadeHeader.Dimensions.CollapsedHeight
+        y = ShadeHeader.Dimensions.CollapsedHeight,
     )
     translate(ShadeHeader.Elements.CollapsedContentEnd, y = ShadeHeader.Dimensions.CollapsedHeight)
     translate(
         ShadeHeader.Elements.ExpandedContent,
-        y = -(ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight)
+        y = -(ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight),
     )
     translate(ShadeHeader.Elements.ShadeCarrierGroup, y = -ShadeHeader.Dimensions.CollapsedHeight)
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt
index de76f70..d35537a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt
@@ -28,8 +28,9 @@
 fun TransitionBuilder.toBouncerTransition() {
     spec = tween(durationMillis = 500)
 
-    distance = UserActionDistance { fromSceneSize, _ ->
-        fromSceneSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION
+    distance = UserActionDistance { fromContent, _, _ ->
+        val fromContentSize = checkNotNull(fromContent.targetSize())
+        fromContentSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION
     }
 
     translate(Bouncer.Elements.Content, y = 300.dp)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index 55fa6ad..e78bc6a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -33,7 +33,10 @@
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance = UserActionDistance { fromSceneSize, _ -> fromSceneSize.height.toFloat() * 2 / 3f }
+    distance = UserActionDistance { fromContent, _, _ ->
+        val fromContentSize = checkNotNull(fromContent.targetSize())
+        fromContentSize.height.toFloat() * 2 / 3f
+    }
 
     translate(OverlayShade.Elements.Panel, Edge.Top)
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index b677dff..bfae489 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -19,12 +19,9 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
 import com.android.systemui.media.controls.ui.composable.MediaCarousel
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
@@ -33,24 +30,16 @@
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.toShadeTransition(
-    durationScale: Double = 1.0,
-) {
+fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
     swipeSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance =
-        object : UserActionDistance {
-            override fun UserActionDistanceScope.absoluteDistance(
-                fromSceneSize: IntSize,
-                orientation: Orientation,
-            ): Float {
-                return Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y ?: 0f
-            }
-        }
+    distance = UserActionDistance { _, _, _ ->
+        Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y ?: 0f
+    }
 
     fractionRange(start = .58f) {
         fade(ShadeHeader.Elements.Clock)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index dbf7d7b..d3ddb50 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -637,8 +637,8 @@
 
 fun interface UserActionDistance {
     /**
-     * Return the **absolute** distance of the user action given the size of the scene we are
-     * animating from and the [orientation].
+     * Return the **absolute** distance of the user action when going from [fromContent] to
+     * [toContent] in the given [orientation].
      *
      * Note: This function will be called for each drag event until it returns a value > 0f. This
      * for instance allows you to return 0f or a negative value until the first layout pass of a
@@ -646,7 +646,8 @@
      * transitioning to when computing this absolute distance.
      */
     fun UserActionDistanceScope.absoluteDistance(
-        fromSceneSize: IntSize,
+        fromContent: ContentKey,
+        toContent: ContentKey,
         orientation: Orientation,
     ): Float
 }
@@ -656,7 +657,8 @@
 /** The user action has a fixed [absoluteDistance]. */
 class FixedDistance(private val distance: Dp) : UserActionDistance {
     override fun UserActionDistanceScope.absoluteDistance(
-        fromSceneSize: IntSize,
+        fromContent: ContentKey,
+        toContent: ContentKey,
         orientation: Orientation,
     ): Float = distance.toPx()
 }
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 7eb5a3f..09156d5 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
@@ -267,11 +267,12 @@
         private set
 
     /**
-     * The flattened list of [SharedElementTransformation] within all the transitions in
+     * The flattened list of [SharedElementTransformation.Factory] within all the transitions in
      * [transitionStates].
      */
-    private val transformationsWithElevation: List<SharedElementTransformation> by derivedStateOf {
-        transformationsWithElevation(transitionStates)
+    private val transformationFactoriesWithElevation:
+        List<SharedElementTransformation.Factory> by derivedStateOf {
+        transformationFactoriesWithElevation(transitionStates)
     }
 
     override val currentScene: SceneKey
@@ -692,22 +693,23 @@
         animate()
     }
 
-    private fun transformationsWithElevation(
+    private fun transformationFactoriesWithElevation(
         transitionStates: List<TransitionState>
-    ): List<SharedElementTransformation> {
+    ): List<SharedElementTransformation.Factory> {
         return buildList {
             transitionStates.fastForEach { state ->
                 if (state !is TransitionState.Transition) {
                     return@fastForEach
                 }
 
-                state.transformationSpec.transformations.fastForEach { transformationWithRange ->
-                    val transformation = transformationWithRange.transformation
+                state.transformationSpec.transformationMatchers.fastForEach { transformationMatcher
+                    ->
+                    val factory = transformationMatcher.factory
                     if (
-                        transformation is SharedElementTransformation &&
-                            transformation.elevateInContent != null
+                        factory is SharedElementTransformation.Factory &&
+                            factory.elevateInContent != null
                     ) {
-                        add(transformation)
+                        add(factory)
                     }
                 }
             }
@@ -722,10 +724,10 @@
      * necessary, for performance.
      */
     internal fun isElevationPossible(content: ContentKey, element: ElementKey?): Boolean {
-        if (transformationsWithElevation.isEmpty()) return false
-        return transformationsWithElevation.fastAny { transformation ->
-            transformation.elevateInContent == content &&
-                (element == null || transformation.matcher.matches(element, content))
+        if (transformationFactoriesWithElevation.isEmpty()) return false
+        return transformationFactoriesWithElevation.fastAny { factory ->
+            factory.elevateInContent == content &&
+                (element == null || factory.matcher.matches(element, content))
         }
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 569593c..8df3f2d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -26,17 +26,9 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
 import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.transformation.CustomAlphaTransformation
-import com.android.compose.animation.scene.transformation.CustomOffsetTransformation
-import com.android.compose.animation.scene.transformation.CustomScaleTransformation
-import com.android.compose.animation.scene.transformation.CustomSizeTransformation
-import com.android.compose.animation.scene.transformation.InterpolatedAlphaTransformation
-import com.android.compose.animation.scene.transformation.InterpolatedOffsetTransformation
-import com.android.compose.animation.scene.transformation.InterpolatedScaleTransformation
-import com.android.compose.animation.scene.transformation.InterpolatedSizeTransformation
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
-import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationMatcher
 import com.android.compose.animation.scene.transformation.TransformationWithRange
 
 /** The transitions configuration of a [SceneTransitionLayout]. */
@@ -169,7 +161,7 @@
 }
 
 /** The definition of a transition between [from] and [to]. */
-interface TransitionSpec {
+internal interface TransitionSpec {
     /** The key of this [TransitionSpec]. */
     val key: TransitionKey?
 
@@ -209,7 +201,7 @@
     fun previewTransformationSpec(transition: TransitionState.Transition): TransformationSpec?
 }
 
-interface TransformationSpec {
+internal interface TransformationSpec {
     /**
      * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
      * the transition is triggered (i.e. it is not gesture-based).
@@ -232,8 +224,8 @@
      */
     val distance: UserActionDistance?
 
-    /** The list of [Transformation] applied to elements during this transition. */
-    val transformations: List<TransformationWithRange<*>>
+    /** The list of [TransformationMatcher] applied to elements during this transformation. */
+    val transformationMatchers: List<TransformationMatcher>
 
     companion object {
         internal val Empty =
@@ -241,7 +233,7 @@
                 progressSpec = snap(),
                 swipeSpec = null,
                 distance = null,
-                transformations = emptyList(),
+                transformationMatchers = emptyList(),
             )
         internal val EmptyProvider = { _: TransitionState.Transition -> Empty }
     }
@@ -272,7 +264,14 @@
                     progressSpec = reverse.progressSpec,
                     swipeSpec = reverse.swipeSpec,
                     distance = reverse.distance,
-                    transformations = reverse.transformations.map { it.reversed() },
+                    transformationMatchers =
+                        reverse.transformationMatchers.map {
+                            TransformationMatcher(
+                                matcher = it.matcher,
+                                factory = it.factory,
+                                range = it.range?.reversed(),
+                            )
+                        },
                 )
             },
         )
@@ -288,7 +287,7 @@
 }
 
 /** The definition of the overscroll behavior of the [content]. */
-interface OverscrollSpec {
+internal interface OverscrollSpec {
     /** The scene we are over scrolling. */
     val content: ContentKey
 
@@ -325,7 +324,7 @@
     override val progressSpec: AnimationSpec<Float>,
     override val swipeSpec: SpringSpec<Float>?,
     override val distance: UserActionDistance?,
-    override val transformations: List<TransformationWithRange<*>>,
+    override val transformationMatchers: List<TransformationMatcher>,
 ) : TransformationSpec {
     private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
 
@@ -335,7 +334,7 @@
             .getOrPut(content) { computeTransformations(element, content) }
     }
 
-    /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
+    /** Filter [transformationMatchers] to compute the [ElementTransformations] of [element]. */
     private fun computeTransformations(
         element: ElementKey,
         content: ContentKey,
@@ -346,46 +345,55 @@
         var drawScale: TransformationWithRange<PropertyTransformation<Scale>>? = null
         var alpha: TransformationWithRange<PropertyTransformation<Float>>? = null
 
-        transformations.fastForEach { transformationWithRange ->
-            val transformation = transformationWithRange.transformation
-            if (!transformation.matcher.matches(element, content)) {
+        transformationMatchers.fastForEach { transformationMatcher ->
+            if (!transformationMatcher.matcher.matches(element, content)) {
                 return@fastForEach
             }
 
-            when (transformation) {
-                is SharedElementTransformation -> {
-                    throwIfNotNull(shared, element, name = "shared")
-                    shared =
-                        transformationWithRange
-                            as TransformationWithRange<SharedElementTransformation>
+            val transformation = transformationMatcher.factory.create()
+            val property =
+                when (transformation) {
+                    is SharedElementTransformation -> {
+                        throwIfNotNull(shared, element, name = "shared")
+                        shared =
+                            TransformationWithRange(transformation, transformationMatcher.range)
+                        return@fastForEach
+                    }
+                    is PropertyTransformation<*> -> transformation.property
                 }
-                is InterpolatedOffsetTransformation,
-                is CustomOffsetTransformation -> {
+
+            when (property) {
+                is PropertyTransformation.Property.Offset -> {
                     throwIfNotNull(offset, element, name = "offset")
                     offset =
-                        transformationWithRange
-                            as TransformationWithRange<PropertyTransformation<Offset>>
+                        TransformationWithRange(
+                            transformation as PropertyTransformation<Offset>,
+                            transformationMatcher.range,
+                        )
                 }
-                is InterpolatedSizeTransformation,
-                is CustomSizeTransformation -> {
+                is PropertyTransformation.Property.Size -> {
                     throwIfNotNull(size, element, name = "size")
                     size =
-                        transformationWithRange
-                            as TransformationWithRange<PropertyTransformation<IntSize>>
+                        TransformationWithRange(
+                            transformation as PropertyTransformation<IntSize>,
+                            transformationMatcher.range,
+                        )
                 }
-                is InterpolatedScaleTransformation,
-                is CustomScaleTransformation -> {
+                is PropertyTransformation.Property.Scale -> {
                     throwIfNotNull(drawScale, element, name = "drawScale")
                     drawScale =
-                        transformationWithRange
-                            as TransformationWithRange<PropertyTransformation<Scale>>
+                        TransformationWithRange(
+                            transformation as PropertyTransformation<Scale>,
+                            transformationMatcher.range,
+                        )
                 }
-                is InterpolatedAlphaTransformation,
-                is CustomAlphaTransformation -> {
+                is PropertyTransformation.Property.Alpha -> {
                     throwIfNotNull(alpha, element, name = "alpha")
                     alpha =
-                        transformationWithRange
-                            as TransformationWithRange<PropertyTransformation<Float>>
+                        TransformationWithRange(
+                            transformation as PropertyTransformation<Float>,
+                            transformationMatcher.range,
+                        )
                 }
             }
         }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index f0043e1..dbfeb5c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -24,7 +24,6 @@
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
 import kotlin.math.absoluteValue
@@ -66,8 +65,9 @@
         val absoluteDistance =
             with(animation.contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) {
                 layoutImpl.userActionDistanceScope.absoluteDistance(
-                    layoutImpl.content(animation.fromContent).targetSize,
-                    orientation,
+                    fromContent = animation.fromContent,
+                    toContent = animation.toContent,
+                    orientation = orientation,
                 )
             }
 
@@ -475,12 +475,14 @@
 
 private object DefaultSwipeDistance : UserActionDistance {
     override fun UserActionDistanceScope.absoluteDistance(
-        fromSceneSize: IntSize,
+        fromContent: ContentKey,
+        toContent: ContentKey,
         orientation: Orientation,
     ): Float {
+        val fromContentSize = checkNotNull(fromContent.targetSize())
         return when (orientation) {
-            Orientation.Horizontal -> fromSceneSize.width
-            Orientation.Vertical -> fromSceneSize.height
+            Orientation.Horizontal -> fromContentSize.width
+            Orientation.Vertical -> fromContentSize.height
         }.toFloat()
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 1fdfca9..48f08a7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
+import com.android.compose.animation.scene.transformation.Transformation
 import kotlin.math.tanh
 
 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
@@ -76,7 +76,7 @@
         preview: (TransitionBuilder.() -> Unit)? = null,
         reversePreview: (TransitionBuilder.() -> Unit)? = null,
         builder: TransitionBuilder.() -> Unit = {},
-    ): TransitionSpec
+    )
 
     /**
      * Define the animation to be played when transitioning [from] the specified content. For the
@@ -102,7 +102,7 @@
         preview: (TransitionBuilder.() -> Unit)? = null,
         reversePreview: (TransitionBuilder.() -> Unit)? = null,
         builder: TransitionBuilder.() -> Unit = {},
-    ): TransitionSpec
+    )
 
     /**
      * Define the animation to be played when the [content] is overscrolled in the given
@@ -115,13 +115,13 @@
         content: ContentKey,
         orientation: Orientation,
         builder: OverscrollBuilder.() -> Unit,
-    ): OverscrollSpec
+    )
 
     /**
      * Prevents overscroll the [content] in the given [orientation], allowing ancestors to
      * eventually consume the remaining gesture.
      */
-    fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec
+    fun overscrollDisabled(content: ContentKey, orientation: Orientation)
 }
 
 interface BaseTransitionBuilder : PropertyTransformationBuilder {
@@ -529,15 +529,8 @@
         anchorHeight: Boolean = true,
     )
 
-    /**
-     * Apply a [CustomPropertyTransformation] to one or more elements.
-     *
-     * @see com.android.compose.animation.scene.transformation.CustomSizeTransformation
-     * @see com.android.compose.animation.scene.transformation.CustomOffsetTransformation
-     * @see com.android.compose.animation.scene.transformation.CustomAlphaTransformation
-     * @see com.android.compose.animation.scene.transformation.CustomScaleTransformation
-     */
-    fun transformation(transformation: CustomPropertyTransformation<*>)
+    /** Apply a [transformation] to the element(s) matching [matcher]. */
+    fun transformation(matcher: ElementMatcher, transformation: Transformation.Factory)
 }
 
 /** This converter lets you change a linear progress into a function of your choice. */
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 79f8cd4..6742b32 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -30,7 +30,6 @@
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.transformation.AnchoredSize
 import com.android.compose.animation.scene.transformation.AnchoredTranslate
-import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
 import com.android.compose.animation.scene.transformation.DrawScale
 import com.android.compose.animation.scene.transformation.EdgeTranslate
 import com.android.compose.animation.scene.transformation.Fade
@@ -38,8 +37,8 @@
 import com.android.compose.animation.scene.transformation.ScaleSize
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
 import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationMatcher
 import com.android.compose.animation.scene.transformation.TransformationRange
-import com.android.compose.animation.scene.transformation.TransformationWithRange
 import com.android.compose.animation.scene.transformation.Translate
 
 internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -67,8 +66,8 @@
         preview: (TransitionBuilder.() -> Unit)?,
         reversePreview: (TransitionBuilder.() -> Unit)?,
         builder: TransitionBuilder.() -> Unit,
-    ): TransitionSpec {
-        return transition(from = null, to = to, key = key, preview, reversePreview, builder)
+    ) {
+        transition(from = null, to = to, key = key, preview, reversePreview, builder)
     }
 
     override fun from(
@@ -78,25 +77,25 @@
         preview: (TransitionBuilder.() -> Unit)?,
         reversePreview: (TransitionBuilder.() -> Unit)?,
         builder: TransitionBuilder.() -> Unit,
-    ): TransitionSpec {
-        return transition(from = from, to = to, key = key, preview, reversePreview, builder)
+    ) {
+        transition(from = from, to = to, key = key, preview, reversePreview, builder)
     }
 
     override fun overscroll(
         content: ContentKey,
         orientation: Orientation,
         builder: OverscrollBuilder.() -> Unit,
-    ): OverscrollSpec {
+    ) {
         val impl = OverscrollBuilderImpl().apply(builder)
-        check(impl.transformations.isNotEmpty()) {
+        check(impl.transformationMatchers.isNotEmpty()) {
             "This method does not allow empty transformations. " +
                 "Use overscrollDisabled($content, $orientation) instead."
         }
-        return overscrollSpec(content, orientation, impl)
+        overscrollSpec(content, orientation, impl)
     }
 
-    override fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec {
-        return overscrollSpec(content, orientation, OverscrollBuilderImpl())
+    override fun overscrollDisabled(content: ContentKey, orientation: Orientation) {
+        overscrollSpec(content, orientation, OverscrollBuilderImpl())
     }
 
     private fun overscrollSpec(
@@ -113,7 +112,7 @@
                         progressSpec = snap(),
                         swipeSpec = null,
                         distance = impl.distance,
-                        transformations = impl.transformations,
+                        transformationMatchers = impl.transformationMatchers,
                     ),
                 progressConverter = impl.progressConverter,
             )
@@ -138,7 +137,7 @@
                 progressSpec = impl.spec,
                 swipeSpec = impl.swipeSpec,
                 distance = impl.distance,
-                transformations = impl.transformations,
+                transformationMatchers = impl.transformationMatchers,
             )
         }
 
@@ -158,7 +157,7 @@
 }
 
 internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
-    val transformations = mutableListOf<TransformationWithRange<*>>()
+    val transformationMatchers = mutableListOf<TransformationMatcher>()
     private var range: TransformationRange? = null
     protected var reversed = false
     override var distance: UserActionDistance? = null
@@ -174,23 +173,31 @@
         range = null
     }
 
-    protected fun addTransformation(transformation: Transformation) {
-        val transformationWithRange = TransformationWithRange(transformation, range)
-        transformations.add(
-            if (reversed) {
-                transformationWithRange.reversed()
-            } else {
-                transformationWithRange
-            }
+    protected fun addTransformation(
+        matcher: ElementMatcher,
+        transformation: Transformation.Factory,
+    ) {
+        transformationMatchers.add(
+            TransformationMatcher(
+                matcher,
+                transformation,
+                range?.let { range ->
+                    if (reversed) {
+                        range.reversed()
+                    } else {
+                        range
+                    }
+                },
+            )
         )
     }
 
     override fun fade(matcher: ElementMatcher) {
-        addTransformation(Fade(matcher))
+        addTransformation(matcher, Fade.Factory)
     }
 
     override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
-        addTransformation(Translate(matcher, x, y))
+        addTransformation(matcher, Translate.Factory(x, y))
     }
 
     override fun translate(
@@ -198,19 +205,19 @@
         edge: Edge,
         startsOutsideLayoutBounds: Boolean,
     ) {
-        addTransformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
+        addTransformation(matcher, EdgeTranslate.Factory(edge, startsOutsideLayoutBounds))
     }
 
     override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
-        addTransformation(AnchoredTranslate(matcher, anchor))
+        addTransformation(matcher, AnchoredTranslate.Factory(anchor))
     }
 
     override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
-        addTransformation(ScaleSize(matcher, width, height))
+        addTransformation(matcher, ScaleSize.Factory(width, height))
     }
 
     override fun scaleDraw(matcher: ElementMatcher, scaleX: Float, scaleY: Float, pivot: Offset) {
-        addTransformation(DrawScale(matcher, scaleX, scaleY, pivot))
+        addTransformation(matcher, DrawScale.Factory(scaleX, scaleY, pivot))
     }
 
     override fun anchoredSize(
@@ -219,12 +226,12 @@
         anchorWidth: Boolean,
         anchorHeight: Boolean,
     ) {
-        addTransformation(AnchoredSize(matcher, anchor, anchorWidth, anchorHeight))
+        addTransformation(matcher, AnchoredSize.Factory(anchor, anchorWidth, anchorHeight))
     }
 
-    override fun transformation(transformation: CustomPropertyTransformation<*>) {
+    override fun transformation(matcher: ElementMatcher, transformation: Transformation.Factory) {
         check(range == null) { "Custom transformations can not be applied inside a range" }
-        addTransformation(transformation)
+        addTransformation(matcher, transformation)
     }
 }
 
@@ -263,7 +270,10 @@
                 "(${transition.toContent.debugName})"
         }
 
-        addTransformation(SharedElementTransformation(matcher, enabled, elevateInContent))
+        addTransformation(
+            matcher,
+            SharedElementTransformation.Factory(matcher, enabled, elevateInContent),
+        )
     }
 
     override fun timestampRange(
@@ -294,6 +304,6 @@
         x: OverscrollScope.() -> Float,
         y: OverscrollScope.() -> Float,
     ) {
-        addTransformation(OverscrollTranslate(matcher, x, y))
+        addTransformation(matcher, OverscrollTranslate.Factory(x, y))
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 38b1aaa..33f015f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -401,7 +401,7 @@
                     else -> null
                 } ?: return true
 
-            return specForCurrentScene.transformationSpec.transformations.isNotEmpty()
+            return specForCurrentScene.transformationSpec.transformationMatchers.isNotEmpty()
         }
 
         internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
new file mode 100644
index 0000000..bfb5ca7
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
@@ -0,0 +1,289 @@
+/*
+ * 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.compose.animation.scene.reveal
+
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.DeferredTargetAnimation
+import androidx.compose.animation.core.ExperimentalAnimatableApi
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceAtMost
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.PropertyTransformationScope
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+
+interface ContainerRevealHaptics {
+    /**
+     * Called when the reveal threshold is crossed while the user was dragging on screen.
+     *
+     * Important: This callback is called during layout and its implementation should therefore be
+     * very fast or posted to a different thread.
+     *
+     * @param revealed whether we go from hidden to revealed, i.e. whether the container size is
+     *   going to jump from a smaller size to a bigger size.
+     */
+    fun onRevealThresholdCrossed(revealed: Boolean)
+}
+
+/** Animate the reveal of [container] by animating its size. */
+fun TransitionBuilder.verticalContainerReveal(
+    container: ElementKey,
+    haptics: ContainerRevealHaptics,
+) {
+    // Make the swipe distance be exactly the target height of the container.
+    // TODO(b/376438969): Make sure that this works correctly when the target size of the element
+    // is changing during the transition (e.g. a notification was added). At the moment, the user
+    // action distance is only called until it returns a value > 0f, which is then cached.
+    distance = UserActionDistance { fromContent, toContent, _ ->
+        val targetSizeInFromContent = container.targetSize(fromContent)
+        val targetSizeInToContent = container.targetSize(toContent)
+        if (targetSizeInFromContent != null && targetSizeInToContent != null) {
+            error(
+                "verticalContainerReveal should not be used with shared elements, but " +
+                    "${container.debugName} is in both ${fromContent.debugName} and " +
+                    toContent.debugName
+            )
+        }
+
+        (targetSizeInToContent?.height ?: targetSizeInFromContent?.height)?.toFloat() ?: 0f
+    }
+
+    // TODO(b/376438969): Improve the motion of this gesture using Motion Mechanics.
+
+    // The min distance to swipe before triggering the reveal spring.
+    val distanceThreshold = 80.dp
+
+    // The minimum height of the container.
+    val minHeight = 10.dp
+
+    // The amount removed from the container width at 0% progress.
+    val widthDelta = 140.dp
+
+    // The ratio at which the distance is tracked before reaching the threshold, e.g. if the user
+    // drags 60dp then the height will be 60dp * 0.25f = 15dp.
+    val trackingRatio = 0.25f
+
+    // The max progress starting from which the container should always be visible, even if we are
+    // animating the container out. This is used so that we don't immediately fade out the container
+    // when triggering a one-off animation that hides it.
+    val alphaProgressThreshold = 0.05f
+
+    // The spring animating the size of the container.
+    val sizeSpec = spring<Float>(stiffness = 380f, dampingRatio = 0.9f)
+
+    // The spring animating the alpha of the container.
+    val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f)
+
+    // The spring animating the progress when releasing the finger.
+    swipeSpec =
+        spring(
+            stiffness = Spring.StiffnessMediumLow,
+            dampingRatio = Spring.DampingRatioNoBouncy,
+            visibilityThreshold = 0.5f,
+        )
+
+    // Size transformation.
+    transformation(container) {
+        VerticalContainerRevealSizeTransformation(
+            haptics,
+            distanceThreshold,
+            trackingRatio,
+            minHeight,
+            widthDelta,
+            sizeSpec,
+        )
+    }
+
+    // Alpha transformation.
+    transformation(container) {
+        ContainerRevealAlphaTransformation(alphaSpec, alphaProgressThreshold)
+    }
+}
+
+@OptIn(ExperimentalAnimatableApi::class)
+private class VerticalContainerRevealSizeTransformation(
+    private val haptics: ContainerRevealHaptics,
+    private val distanceThreshold: Dp,
+    private val trackingRatio: Float,
+    private val minHeight: Dp,
+    private val widthDelta: Dp,
+    private val spec: FiniteAnimationSpec<Float>,
+) : CustomPropertyTransformation<IntSize> {
+    override val property = PropertyTransformation.Property.Size
+
+    private val widthAnimation = DeferredTargetAnimation(Float.VectorConverter)
+    private val heightAnimation = DeferredTargetAnimation(Float.VectorConverter)
+
+    private var previousHasReachedThreshold: Boolean? = null
+
+    override fun PropertyTransformationScope.transform(
+        content: ContentKey,
+        element: ElementKey,
+        transition: TransitionState.Transition,
+        transitionScope: CoroutineScope,
+    ): IntSize {
+        // The distance to go to 100%. Note that we don't use
+        // TransitionState.HasOverscrollProperties.absoluteDistance because the transition will not
+        // implement HasOverscrollProperties if the transition is triggered and not gesture based.
+        val idleSize = checkNotNull(element.targetSize(content))
+        val userActionDistance = idleSize.height
+        val progress =
+            when ((transition as? TransitionState.HasOverscrollProperties)?.bouncingContent) {
+                null -> transition.progressTo(content)
+                content -> 1f
+                else -> 0f
+            }
+        val distance = (progress * userActionDistance).fastCoerceAtLeast(0f)
+        val threshold = distanceThreshold.toPx()
+
+        // Width.
+        val widthDelta = widthDelta.toPx()
+        val width =
+            (idleSize.width - widthDelta +
+                    animateSize(
+                        size = widthDelta,
+                        distance = distance,
+                        threshold = threshold,
+                        transitionScope = transitionScope,
+                        animation = widthAnimation,
+                    ))
+                .roundToInt()
+
+        // Height.
+        val minHeight = minHeight.toPx()
+        val height =
+            (
+                // 1) The minimum size of the container.
+                minHeight +
+
+                    // 2) The animated size between the minimum size and the threshold.
+                    animateSize(
+                        size = threshold - minHeight,
+                        distance = distance,
+                        threshold = threshold,
+                        transitionScope = transitionScope,
+                        animation = heightAnimation,
+                    ) +
+
+                    // 3) The remaining height after the threshold, tracking the finger.
+                    (distance - threshold).fastCoerceAtLeast(0f))
+                .roundToInt()
+                .fastCoerceAtMost(idleSize.height)
+
+        // Haptics.
+        val hasReachedThreshold = distance >= threshold
+        if (
+            previousHasReachedThreshold != null &&
+                hasReachedThreshold != previousHasReachedThreshold &&
+                transition.isUserInputOngoing
+        ) {
+            haptics.onRevealThresholdCrossed(revealed = hasReachedThreshold)
+        }
+        previousHasReachedThreshold = hasReachedThreshold
+
+        return IntSize(width = width, height = height)
+    }
+
+    /**
+     * Animate a size up to [size], so that it is equal to 0f when distance is 0f and equal to
+     * [size] when `distance >= threshold`, taking the [trackingRatio] into account.
+     */
+    @OptIn(ExperimentalAnimatableApi::class)
+    private fun animateSize(
+        size: Float,
+        distance: Float,
+        threshold: Float,
+        transitionScope: CoroutineScope,
+        animation: DeferredTargetAnimation<Float, AnimationVector1D>,
+    ): Float {
+        val trackingSize = distance.fastCoerceAtMost(threshold) / threshold * size * trackingRatio
+        val springTarget =
+            if (distance >= threshold) {
+                size * (1f - trackingRatio)
+            } else {
+                0f
+            }
+        val springSize = animation.updateTarget(springTarget, transitionScope, spec)
+        return trackingSize + springSize
+    }
+}
+
+@OptIn(ExperimentalAnimatableApi::class)
+private class ContainerRevealAlphaTransformation(
+    private val spec: FiniteAnimationSpec<Float>,
+    private val progressThreshold: Float,
+) : CustomPropertyTransformation<Float> {
+    override val property = PropertyTransformation.Property.Alpha
+    private val alphaAnimation = DeferredTargetAnimation(Float.VectorConverter)
+
+    override fun PropertyTransformationScope.transform(
+        content: ContentKey,
+        element: ElementKey,
+        transition: TransitionState.Transition,
+        transitionScope: CoroutineScope,
+    ): Float {
+        return alphaAnimation.updateTarget(targetAlpha(transition, content), transitionScope, spec)
+    }
+
+    private fun targetAlpha(transition: TransitionState.Transition, content: ContentKey): Float {
+        if (transition.isUserInputOngoing) {
+            if (transition !is TransitionState.HasOverscrollProperties) {
+                error(
+                    "Unsupported transition driven by user input but that does not have " +
+                        "overscroll properties: $transition"
+                )
+            }
+
+            val bouncingContent = transition.bouncingContent
+            return if (bouncingContent != null) {
+                if (bouncingContent == content) 1f else 0f
+            } else {
+                if (transition.progressTo(content) > 0f) 1f else 0f
+            }
+        }
+
+        // The transition was committed (the user released their finger), so the alpha depends on
+        // whether we are animating towards the content (showing the container) or away from it
+        // (hiding the container).
+        val isShowingContainer =
+            when (content) {
+                is SceneKey -> transition.currentScene == content
+                is OverlayKey -> transition.currentOverlays.contains(content)
+            }
+
+        return if (isShowingContainer || transition.progressTo(content) >= progressThreshold) {
+            1f
+        } else {
+            0f
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 85bb533..6575068 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -19,16 +19,17 @@
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Anchor the size of an element to the size of another element. */
-internal class AnchoredSize(
-    override val matcher: ElementMatcher,
+internal class AnchoredSize
+private constructor(
     private val anchor: ElementKey,
     private val anchorWidth: Boolean,
     private val anchorHeight: Boolean,
-) : InterpolatedSizeTransformation {
+) : InterpolatedPropertyTransformation<IntSize> {
+    override val property = PropertyTransformation.Property.Size
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -59,4 +60,12 @@
             anchorSizeIn(transition.fromContent)
         }
     }
+
+    class Factory(
+        private val anchor: ElementKey,
+        private val anchorWidth: Boolean,
+        private val anchorHeight: Boolean,
+    ) : Transformation.Factory {
+        override fun create(): Transformation = AnchoredSize(anchor, anchorWidth, anchorHeight)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 04cd683..890902b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -19,14 +19,13 @@
 import androidx.compose.ui.geometry.Offset
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Anchor the translation of an element to another element. */
-internal class AnchoredTranslate(
-    override val matcher: ElementMatcher,
-    private val anchor: ElementKey,
-) : InterpolatedOffsetTransformation {
+internal class AnchoredTranslate private constructor(private val anchor: ElementKey) :
+    InterpolatedPropertyTransformation<Offset> {
+    override val property = PropertyTransformation.Property.Offset
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -56,6 +55,10 @@
             Offset(idleValue.x + offset.x, idleValue.y + offset.y)
         }
     }
+
+    class Factory(private val anchor: ElementKey) : Transformation.Factory {
+        override fun create(): Transformation = AnchoredTranslate(anchor)
+    }
 }
 
 internal fun throwMissingAnchorException(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 45d6d40..347f1c3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -19,7 +19,6 @@
 import androidx.compose.ui.geometry.Offset
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.Scale
 import com.android.compose.animation.scene.content.state.TransitionState
 
@@ -27,12 +26,14 @@
  * Scales the draw size of an element. Note this will only scale the draw inside of an element,
  * therefore it won't impact layout of elements around it.
  */
-internal class DrawScale(
-    override val matcher: ElementMatcher,
+internal class DrawScale
+private constructor(
     private val scaleX: Float,
     private val scaleY: Float,
-    private val pivot: Offset = Offset.Unspecified,
-) : InterpolatedScaleTransformation {
+    private val pivot: Offset,
+) : InterpolatedPropertyTransformation<Scale> {
+    override val property = PropertyTransformation.Property.Scale
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -41,4 +42,9 @@
     ): Scale {
         return Scale(scaleX, scaleY, pivot)
     }
+
+    class Factory(private val scaleX: Float, private val scaleY: Float, private val pivot: Offset) :
+        Transformation.Factory {
+        override fun create(): Transformation = DrawScale(scaleX, scaleY, pivot)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 21d66d7..f8e6dc0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -20,15 +20,14 @@
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Translate an element from an edge of the layout. */
-internal class EdgeTranslate(
-    override val matcher: ElementMatcher,
-    private val edge: Edge,
-    private val startsOutsideLayoutBounds: Boolean = true,
-) : InterpolatedOffsetTransformation {
+internal class EdgeTranslate
+private constructor(private val edge: Edge, private val startsOutsideLayoutBounds: Boolean) :
+    InterpolatedPropertyTransformation<Offset> {
+    override val property = PropertyTransformation.Property.Offset
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -67,4 +66,9 @@
                 }
         }
     }
+
+    class Factory(private val edge: Edge, private val startsOutsideLayoutBounds: Boolean) :
+        Transformation.Factory {
+        override fun create(): Transformation = EdgeTranslate(edge, startsOutsideLayoutBounds)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index d942273..d92419e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -18,11 +18,12 @@
 
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Fade an element in or out. */
-internal class Fade(override val matcher: ElementMatcher) : InterpolatedAlphaTransformation {
+internal object Fade : InterpolatedPropertyTransformation<Float> {
+    override val property = PropertyTransformation.Property.Alpha
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -33,4 +34,8 @@
         // fading out, which is `0` in both cases.
         return 0f
     }
+
+    object Factory : Transformation.Factory {
+        override fun create(): Transformation = Fade
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index 5f3cdab..5d31fd9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -19,7 +19,6 @@
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.content.state.TransitionState
 import kotlin.math.roundToInt
 
@@ -27,11 +26,10 @@
  * Scales the size of an element. Note that this makes the element resize every frame and will
  * therefore impact the layout of other elements.
  */
-internal class ScaleSize(
-    override val matcher: ElementMatcher,
-    private val width: Float = 1f,
-    private val height: Float = 1f,
-) : InterpolatedSizeTransformation {
+internal class ScaleSize private constructor(private val width: Float, private val height: Float) :
+    InterpolatedPropertyTransformation<IntSize> {
+    override val property = PropertyTransformation.Property.Size
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -43,4 +41,9 @@
             height = (idleValue.height * height).roundToInt(),
         )
     }
+
+    class Factory(private val width: Float = 1f, private val height: Float = 1f) :
+        Transformation.Factory {
+        override fun create(): Transformation = ScaleSize(width, height)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index d5143d7..e0b4218 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -35,26 +35,59 @@
 
 /** A transformation applied to one or more elements during a transition. */
 sealed interface Transformation {
-    /**
-     * The matcher that should match the element(s) to which this transformation should be applied.
-     */
-    val matcher: ElementMatcher
+    fun interface Factory {
+        fun create(): Transformation
+    }
 }
 
-internal class SharedElementTransformation(
-    override val matcher: ElementMatcher,
+// Important: SharedElementTransformation must be a data class because we check that we don't
+// provide 2 different transformations for the same element in Element.kt
+internal data class SharedElementTransformation(
     internal val enabled: Boolean,
     internal val elevateInContent: ContentKey?,
-) : Transformation
+) : Transformation {
+    class Factory(
+        internal val matcher: ElementMatcher,
+        internal val enabled: Boolean,
+        internal val elevateInContent: ContentKey?,
+    ) : Transformation.Factory {
+        override fun create(): Transformation {
+            return SharedElementTransformation(enabled, elevateInContent)
+        }
+    }
+}
 
-/** A transformation that changes the value of an element property, like its size or offset. */
-sealed interface PropertyTransformation<T> : Transformation
+/**
+ * A transformation that changes the value of an element [Property], like its [size][Property.Size]
+ * or [offset][Property.Offset].
+ */
+sealed interface PropertyTransformation<T> : Transformation {
+    /** The property to which this transformation is applied. */
+    val property: Property<T>
+
+    sealed class Property<T> {
+        /** The size of an element. */
+        data object Size : Property<IntSize>()
+
+        /** The offset (position) of an element. */
+        data object Offset : Property<androidx.compose.ui.geometry.Offset>()
+
+        /** The alpha of an element. */
+        data object Alpha : Property<Float>()
+
+        /**
+         * The drawing scale of an element. Animating the scale does not have any effect on the
+         * layout.
+         */
+        data object Scale : Property<com.android.compose.animation.scene.Scale>()
+    }
+}
 
 /**
  * A transformation to a target/transformed value that is automatically interpolated using the
  * transition progress and transformation range.
  */
-sealed interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> {
+interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> {
     /**
      * Return the transformed value for the given property, i.e.:
      * - the value at progress = 0% for elements that are entering the layout (i.e. elements in the
@@ -73,19 +106,7 @@
     ): T
 }
 
-/** An [InterpolatedPropertyTransformation] applied to the size of one or more elements. */
-interface InterpolatedSizeTransformation : InterpolatedPropertyTransformation<IntSize>
-
-/** An [InterpolatedPropertyTransformation] applied to the offset of one or more elements. */
-interface InterpolatedOffsetTransformation : InterpolatedPropertyTransformation<Offset>
-
-/** An [InterpolatedPropertyTransformation] applied to the alpha of one or more elements. */
-interface InterpolatedAlphaTransformation : InterpolatedPropertyTransformation<Float>
-
-/** An [InterpolatedPropertyTransformation] applied to the scale of one or more elements. */
-interface InterpolatedScaleTransformation : InterpolatedPropertyTransformation<Scale>
-
-sealed interface CustomPropertyTransformation<T> : PropertyTransformation<T> {
+interface CustomPropertyTransformation<T> : PropertyTransformation<T> {
     /**
      * Return the value that the property should have in the current frame for the given [content]
      * and [element].
@@ -105,25 +126,20 @@
     ): T
 }
 
-/** A [CustomPropertyTransformation] applied to the size of one or more elements. */
-interface CustomSizeTransformation : CustomPropertyTransformation<IntSize>
-
-/** A [CustomPropertyTransformation] applied to the offset of one or more elements. */
-interface CustomOffsetTransformation : CustomPropertyTransformation<Offset>
-
-/** A [CustomPropertyTransformation] applied to the alpha of one or more elements. */
-interface CustomAlphaTransformation : CustomPropertyTransformation<Float>
-
-/** A [CustomPropertyTransformation] applied to the scale of one or more elements. */
-interface CustomScaleTransformation : CustomPropertyTransformation<Scale>
-
 interface PropertyTransformationScope : Density, ElementStateScope {
     /** The current [direction][LayoutDirection] of the layout. */
     val layoutDirection: LayoutDirection
 }
 
+/** Defines the transformation-type to be applied to all elements matching [matcher]. */
+internal class TransformationMatcher(
+    val matcher: ElementMatcher,
+    val factory: Transformation.Factory,
+    val range: TransformationRange?,
+)
+
 /** A pair consisting of a [transformation] and optional [range]. */
-class TransformationWithRange<out T : Transformation>(
+internal data class TransformationWithRange<out T : Transformation>(
     val transformation: T,
     val range: TransformationRange?,
 ) {
@@ -135,7 +151,7 @@
 }
 
 /** The progress-based range of a [PropertyTransformation]. */
-data class TransformationRange(val start: Float, val end: Float, val easing: Easing) {
+internal data class TransformationRange(val start: Float, val end: Float, val easing: Easing) {
     constructor(
         start: Float? = null,
         end: Float? = null,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index d756c86..2f4d5bff 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -19,18 +19,15 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.OverscrollScope
 import com.android.compose.animation.scene.content.state.TransitionState
 
-internal class Translate(
-    override val matcher: ElementMatcher,
-    private val x: Dp = 0.dp,
-    private val y: Dp = 0.dp,
-) : InterpolatedOffsetTransformation {
+internal class Translate private constructor(private val x: Dp, private val y: Dp) :
+    InterpolatedPropertyTransformation<Offset> {
+    override val property = PropertyTransformation.Property.Offset
+
     override fun PropertyTransformationScope.transform(
         content: ContentKey,
         element: ElementKey,
@@ -39,13 +36,19 @@
     ): Offset {
         return Offset(idleValue.x + x.toPx(), idleValue.y + y.toPx())
     }
+
+    class Factory(private val x: Dp, private val y: Dp) : Transformation.Factory {
+        override fun create(): Transformation = Translate(x, y)
+    }
 }
 
-internal class OverscrollTranslate(
-    override val matcher: ElementMatcher,
-    val x: OverscrollScope.() -> Float = { 0f },
-    val y: OverscrollScope.() -> Float = { 0f },
-) : InterpolatedOffsetTransformation {
+internal class OverscrollTranslate
+private constructor(
+    private val x: OverscrollScope.() -> Float,
+    private val y: OverscrollScope.() -> Float,
+) : InterpolatedPropertyTransformation<Offset> {
+    override val property = PropertyTransformation.Property.Offset
+
     private val cachedOverscrollScope = CachedOverscrollScope()
 
     override fun PropertyTransformationScope.transform(
@@ -63,6 +66,13 @@
 
         return Offset(x = value.x + overscrollScope.x(), y = value.y + overscrollScope.y())
     }
+
+    class Factory(
+        private val x: OverscrollScope.() -> Float,
+        private val y: OverscrollScope.() -> Float,
+    ) : Transformation.Factory {
+        override fun create(): Transformation = OverscrollTranslate(x, y)
+    }
 }
 
 /**
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 61e9bb0..10057b2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -644,7 +644,7 @@
         mutableUserActionsA = emptyMap()
         mutableUserActionsB = emptyMap()
 
-        // start accelaratedScroll and scroll over to B -> null
+        // start acceleratedScroll and scroll over to B -> null
         val dragController2 = onDragStartedImmediately()
         dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)
         dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)
@@ -1530,7 +1530,7 @@
     fun interceptingTransitionKeepsDistance() = runGestureTest {
         var swipeDistance = 75f
         layoutState.transitions = transitions {
-            from(SceneA, to = SceneB) { distance = UserActionDistance { _, _ -> swipeDistance } }
+            from(SceneA, to = SceneB) { distance = UserActionDistance { _, _, _ -> swipeDistance } }
         }
 
         // Start transition.
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 57ad81e..397ca0e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -153,7 +153,7 @@
 
         // Default transition from A to B.
         assertThat(state.setTargetScene(SceneB, animationScope = this)).isNotNull()
-        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
+        assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(1)
 
         // Go back to A.
         state.setTargetScene(SceneA, animationScope = this)
@@ -166,7 +166,7 @@
                 state.setTargetScene(SceneB, animationScope = this, transitionKey = transitionkey)
             )
             .isNotNull()
-        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
+        assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(2)
     }
 
     @Test
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 97a96a4..b3a3261 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -55,7 +55,6 @@
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -672,12 +671,12 @@
         }
 
         assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
-        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
+        assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(1)
 
         // Move the pointer up to swipe to scene B using the new transition.
         rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
         assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
-        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
+        assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(2)
     }
 
     @Test
@@ -685,7 +684,8 @@
         val swipeDistance =
             object : UserActionDistance {
                 override fun UserActionDistanceScope.absoluteDistance(
-                    fromSceneSize: IntSize,
+                    fromContent: ContentKey,
+                    toContent: ContentKey,
                     orientation: Orientation,
                 ): Float {
                     // Foo is going to have a vertical offset of 50dp. Let's make the swipe distance
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index 1f9ba9e..70f2ff80 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -28,11 +28,12 @@
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
 import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.transformation.CustomSizeTransformation
+import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
 import com.android.compose.animation.scene.transformation.OverscrollTranslate
+import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.PropertyTransformationScope
+import com.android.compose.animation.scene.transformation.TransformationMatcher
 import com.android.compose.animation.scene.transformation.TransformationRange
-import com.android.compose.animation.scene.transformation.TransformationWithRange
 import com.android.compose.test.transition
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
@@ -103,7 +104,7 @@
         val transitions = transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }
 
         val transformations =
-            transitions.transitionSpecs.single().transformationSpec(aToB()).transformations
+            transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers
         assertThat(transformations.size).isEqualTo(1)
         assertThat(transformations.single().range).isEqualTo(null)
     }
@@ -126,7 +127,7 @@
         }
 
         val transformations =
-            transitions.transitionSpecs.single().transformationSpec(aToB()).transformations
+            transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers
         assertThat(transformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
             .containsExactly(
@@ -157,7 +158,7 @@
         }
 
         val transformations =
-            transitions.transitionSpecs.single().transformationSpec(aToB()).transformations
+            transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers
         assertThat(transformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
             .containsExactly(
@@ -185,7 +186,7 @@
         }
 
         val transformations =
-            transitions.transitionSpecs.single().transformationSpec(aToB()).transformations
+            transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers
         assertThat(transformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
             .containsExactly(
@@ -215,7 +216,7 @@
         // to B we defined.
         val transitionSpec = transitions.transitionSpec(from = SceneB, to = SceneA, key = null)
 
-        val transformations = transitionSpec.transformationSpec(aToB()).transformations
+        val transformations = transitionSpec.transformationSpec(aToB()).transformationMatchers
 
         assertThat(transformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
@@ -225,7 +226,7 @@
             )
 
         val previewTransformations =
-            transitionSpec.previewTransformationSpec(aToB())?.transformations
+            transitionSpec.previewTransformationSpec(aToB())?.transformationMatchers
 
         assertThat(previewTransformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
@@ -255,7 +256,7 @@
                 key = TransitionKey.PredictiveBack,
             )
 
-        val transformations = transitionSpec.transformationSpec(aToB()).transformations
+        val transformations = transitionSpec.transformationSpec(aToB()).transformationMatchers
 
         assertThat(transformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
@@ -265,7 +266,7 @@
             )
 
         val previewTransformations =
-            transitionSpec.previewTransformationSpec(aToB())?.transformations
+            transitionSpec.previewTransformationSpec(aToB())?.transformationMatchers
 
         assertThat(previewTransformations)
             .comparingElementsUsing(TRANSFORMATION_RANGE)
@@ -316,7 +317,7 @@
 
         val overscrollSpec = transitions.overscrollSpecs.single()
         val transformation =
-            overscrollSpec.transformationSpec.transformations.single().transformation
+            overscrollSpec.transformationSpec.transformationMatchers.single().factory.create()
         assertThat(transformation).isInstanceOf(OverscrollTranslate::class.java)
     }
 
@@ -324,7 +325,7 @@
     fun overscrollSpec_for_overscrollDisabled() {
         val transitions = transitions { overscrollDisabled(SceneA, Orientation.Vertical) }
         val overscrollSpec = transitions.overscrollSpecs.single()
-        assertThat(overscrollSpec.transformationSpec.transformations).isEmpty()
+        assertThat(overscrollSpec.transformationSpec.transformationMatchers).isEmpty()
     }
 
     @Test
@@ -353,9 +354,9 @@
         val transitions = transitions {
             from(SceneA, to = SceneB) {
                 fractionRange {
-                    transformation(
-                        object : CustomSizeTransformation {
-                            override val matcher: ElementMatcher = TestElements.Foo
+                    transformation(TestElements.Foo) {
+                        object : CustomPropertyTransformation<IntSize> {
+                            override val property = PropertyTransformation.Property.Size
 
                             override fun PropertyTransformationScope.transform(
                                 content: ContentKey,
@@ -364,7 +365,7 @@
                                 transitionScope: CoroutineScope,
                             ): IntSize = IntSize.Zero
                         }
-                    )
+                    }
                 }
             }
         }
@@ -377,7 +378,7 @@
 
     companion object {
         private val TRANSFORMATION_RANGE =
-            Correspondence.transforming<TransformationWithRange<*>, TransformationRange?>(
+            Correspondence.transforming<TransformationMatcher, TransformationRange?>(
                 { it?.range },
                 "has range equal to",
             )
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
index 313379f..0adb480 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -157,7 +157,7 @@
         check("isUserInputOngoing").that(actual.isUserInputOngoing).isEqualTo(isUserInputOngoing)
     }
 
-    fun hasOverscrollSpec(): OverscrollSpec {
+    internal fun hasOverscrollSpec(): OverscrollSpec {
         check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNotNull()
         return actual.currentOverscrollSpec!!
     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt
index 487b099..444ec4e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt
@@ -30,7 +30,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
-import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.TestElements
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.testTransition
@@ -47,8 +46,9 @@
     @Test
     fun customSize() {
         /** A size transformation that adds [add] to the size of the transformed element(s). */
-        class AddSizeTransformation(override val matcher: ElementMatcher, private val add: Dp) :
-            CustomSizeTransformation {
+        class AddSizeTransformation(private val add: Dp) : CustomPropertyTransformation<IntSize> {
+            override val property = PropertyTransformation.Property.Size
+
             override fun PropertyTransformationScope.transform(
                 content: ContentKey,
                 element: ElementKey,
@@ -69,7 +69,7 @@
                 spec = tween(16 * 4, easing = LinearEasing)
 
                 // Add 80dp to the width and height of Foo.
-                transformation(AddSizeTransformation(TestElements.Foo, 80.dp))
+                transformation(TestElements.Foo) { AddSizeTransformation(80.dp) }
             },
         ) {
             before { onElement(TestElements.Foo).assertSizeIsEqualTo(40.dp, 20.dp) }
@@ -84,8 +84,9 @@
     @Test
     fun customOffset() {
         /** An offset transformation that adds [add] to the offset of the transformed element(s). */
-        class AddOffsetTransformation(override val matcher: ElementMatcher, private val add: Dp) :
-            CustomOffsetTransformation {
+        class AddOffsetTransformation(private val add: Dp) : CustomPropertyTransformation<Offset> {
+            override val property = PropertyTransformation.Property.Offset
+
             override fun PropertyTransformationScope.transform(
                 content: ContentKey,
                 element: ElementKey,
@@ -106,7 +107,7 @@
                 spec = tween(16 * 4, easing = LinearEasing)
 
                 // Add 80dp to the offset of Foo.
-                transformation(AddOffsetTransformation(TestElements.Foo, 80.dp))
+                transformation(TestElements.Foo) { AddOffsetTransformation(80.dp) }
             },
         ) {
             before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 0.dp) }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
deleted file mode 100644
index 2a2d333..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
+++ /dev/null
@@ -1,252 +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.systemui.shared.clocks
-
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Typeface
-import android.graphics.drawable.Drawable
-import android.util.TypedValue
-import com.android.internal.policy.SystemBarUtils
-import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
-import com.android.systemui.monet.Style as MonetStyle
-import java.io.IOException
-
-class AssetLoader
-private constructor(
-    private val pluginCtx: Context,
-    private val sysuiCtx: Context,
-    private val baseDir: String,
-    var seedColor: Int?,
-    var overrideChroma: Float?,
-    val typefaceCache: TypefaceCache,
-    messageBuffer: MessageBuffer,
-) {
-    val logger = Logger(messageBuffer, TAG)
-    private val resources =
-        listOf(
-            Pair(pluginCtx.resources, pluginCtx.packageName),
-            Pair(sysuiCtx.resources, sysuiCtx.packageName),
-        )
-
-    constructor(
-        pluginCtx: Context,
-        sysuiCtx: Context,
-        baseDir: String,
-        messageBuffer: MessageBuffer,
-    ) : this(
-        pluginCtx,
-        sysuiCtx,
-        baseDir,
-        seedColor = null,
-        overrideChroma = null,
-        typefaceCache =
-            TypefaceCache(messageBuffer) {
-                // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
-                return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
-            },
-        messageBuffer = messageBuffer,
-    )
-
-    fun listAssets(path: String): List<String> {
-        return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList()
-    }
-
-    fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString)
-
-    fun readString(resStr: String): String {
-        val resPair = resolveResourceId(resStr)
-        if (resPair == null) {
-            throw IOException("Failed to parse string: $resStr")
-        }
-
-        val (res, id) = resPair
-        return res.getString(id)
-    }
-
-    fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)
-
-    fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
-
-    fun readTextAsset(path: String): String {
-        return pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
-            val buffer = ByteArray(stream.available())
-            stream.read(buffer)
-            String(buffer)
-        }
-    }
-
-    fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset)
-
-    fun readDrawableAsset(path: String): Drawable {
-        var result: Drawable?
-
-        if (path.startsWith("@")) {
-            val pair = resolveResourceId(path)
-            if (pair == null) {
-                throw IOException("Failed to parse $path to an id")
-            }
-            val (res, id) = pair
-            result = res.getDrawable(id)
-        } else if (path.endsWith("xml")) {
-            // TODO(b/248609434): Support xml files in assets
-            throw IOException("Cannot load xml files from assets")
-        } else {
-            // Attempt to load as if it's a bitmap and directly loadable
-            result =
-                pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
-                    Drawable.createFromResourceStream(
-                        pluginCtx.resources,
-                        TypedValue(),
-                        stream,
-                        null,
-                    )
-                }
-        }
-
-        return result ?: throw IOException("Failed to load: $baseDir$path")
-    }
-
-    fun parseResourceId(resStr: String): Triple<String?, String, String> {
-        if (!resStr.startsWith("@")) {
-            throw IOException("Invalid resource id: $resStr; Must start with '@'")
-        }
-
-        // Parse out resource string
-        val parts = resStr.drop(1).split('/', ':')
-        return when (parts.size) {
-            2 -> Triple(null, parts[0], parts[1])
-            3 -> Triple(parts[0], parts[1], parts[2])
-            else -> throw IOException("Failed to parse resource string: $resStr")
-        }
-    }
-
-    fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
-        val (packageName, category, name) = parseResourceId(resStr)
-        return resolveResourceId(packageName, category, name)
-    }
-
-    fun resolveResourceId(
-        packageName: String?,
-        category: String,
-        name: String,
-    ): Pair<Resources, Int>? {
-        for ((res, ctxPkgName) in resources) {
-            val result = res.getIdentifier(name, category, packageName ?: ctxPkgName)
-            if (result != 0) {
-                return Pair(res, result)
-            }
-        }
-        return null
-    }
-
-    private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? {
-        try {
-            if (arg == null) {
-                return null
-            }
-            return fn(arg)
-        } catch (ex: IOException) {
-            logger.w("Failed to read $arg", ex)
-            return null
-        }
-    }
-
-    fun assetExists(path: String): Boolean {
-        try {
-            if (path.startsWith("@")) {
-                val pair = resolveResourceId(path)
-                return pair != null
-            } else {
-                val stream = pluginCtx.resources.assets.open("$baseDir$path")
-                if (stream == null) {
-                    return false
-                }
-
-                stream.close()
-                return true
-            }
-        } catch (ex: IOException) {
-            return false
-        }
-    }
-
-    fun copy(messageBuffer: MessageBuffer? = null): AssetLoader =
-        AssetLoader(
-            pluginCtx,
-            sysuiCtx,
-            baseDir,
-            seedColor,
-            overrideChroma,
-            typefaceCache,
-            messageBuffer ?: logger.buffer,
-        )
-
-    fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
-        this.seedColor = seedColor
-    }
-
-    fun getClockPaddingStart(): Int {
-        val result = resolveResourceId(null, "dimen", "clock_padding_start")
-        if (result != null) {
-            val (res, id) = result
-            return res.getDimensionPixelSize(id)
-        }
-        return -1
-    }
-
-    fun getStatusBarHeight(): Int {
-        val display = pluginCtx.getDisplayNoVerify()
-        if (display != null) {
-            return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout)
-        }
-
-        logger.w("No display available; falling back to android.R.dimen.status_bar_height")
-        val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height")
-        if (statusBarHeight != null) {
-            val (res, resId) = statusBarHeight
-            return res.getDimensionPixelSize(resId)
-        }
-
-        throw Exception("Could not fetch StatusBarHeight")
-    }
-
-    fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id }
-
-    fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize)
-
-    fun getString(name: String): String = getResource("string", name, Resources::getString)
-
-    private fun <T> getResource(
-        category: String,
-        name: String,
-        getter: (res: Resources, id: Int) -> T,
-    ): T {
-        val result = resolveResourceId(null, category, name)
-        if (result != null) {
-            val (res, id) = result
-            if (id == -1) throw Exception("Cannot find id of $id from $TAG")
-            return getter(res, id)
-        }
-        throw Exception("Cannot find id of $name from $TAG")
-    }
-
-    companion object {
-        private val TAG = AssetLoader::class.simpleName!!
-    }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index 4ed8fd8..d0a32dc 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -16,12 +16,9 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -37,31 +34,22 @@
 import java.util.TimeZone
 
 class ComposedDigitalLayerController(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     private val layer: ComposedDigitalHandLayer,
-    messageBuffer: MessageBuffer,
 ) : SimpleClockLayerController {
-    private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
+    private val logger =
+        Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
 
     val layerControllers = mutableListOf<SimpleClockLayerController>()
     val dozeState = DefaultClockController.AnimationState(1F)
 
-    override val view = FlexClockView(ctx, assets, messageBuffer)
+    override val view = FlexClockView(clockCtx)
 
     init {
         layer.digitalLayers.forEach {
-            val childView = SimpleDigitalClockTextView(ctx, messageBuffer)
+            val childView = SimpleDigitalClockTextView(clockCtx)
             val controller =
-                SimpleDigitalHandLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    it as DigitalHandLayer,
-                    childView,
-                    messageBuffer,
-                )
+                SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView)
 
             view.addView(childView)
             layerControllers.add(controller)
@@ -156,8 +144,9 @@
                 val color =
                     when {
                         theme.seedColor != null -> theme.seedColor!!
-                        theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100)
-                        else -> resources.getColor(android.R.color.system_accent2_600)
+                        theme.isDarkTheme ->
+                            clockCtx.resources.getColor(android.R.color.system_accent1_100)
+                        else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
                     }
 
                 view.updateColor(color)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index be4ebdf..935737c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -15,10 +15,10 @@
 
 import android.content.Context
 import android.content.res.Resources
+import android.graphics.Typeface
 import android.view.LayoutInflater
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFontAxis
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
@@ -33,6 +33,15 @@
 private val TAG = DefaultClockProvider::class.simpleName
 const val DEFAULT_CLOCK_ID = "DEFAULT"
 
+data class ClockContext(
+    val context: Context,
+    val resources: Resources,
+    val settings: ClockSettings,
+    val typefaceCache: TypefaceCache,
+    val messageBuffers: ClockMessageBuffers,
+    val messageBuffer: MessageBuffer,
+)
+
 /** Provides the default system clock */
 class DefaultClockProvider(
     val ctx: Context,
@@ -55,18 +64,24 @@
         }
 
         return if (isClockReactiveVariantsEnabled) {
-            val buffer =
-                messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO)
-            val assets = AssetLoader(ctx, ctx, "clocks/", buffer)
-            assets.setSeedColor(settings.seedColor, null)
+            val buffers = messageBuffers ?: ClockMessageBuffers(LogUtil.DEFAULT_MESSAGE_BUFFER)
             val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes)
+            val clockSettings = settings.copy(axes = fontAxes.map { it.toSetting() })
+            val typefaceCache =
+                TypefaceCache(buffers.infraMessageBuffer) {
+                    // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
+                    return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
+                }
             FlexClockController(
-                ctx,
-                resources,
-                settings.copy(axes = fontAxes.map { it.toSetting() }),
-                assets,
+                ClockContext(
+                    ctx,
+                    resources,
+                    clockSettings,
+                    typefaceCache,
+                    buffers,
+                    buffers.infraMessageBuffer,
+                ),
                 FLEX_DESIGN,
-                messageBuffers,
             )
         } else {
             DefaultClockController(
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 6c627e2..c7a3f63 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import com.android.systemui.customization.R
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.AxisType
@@ -26,8 +24,6 @@
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFontAxis
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.plugins.clocks.ClockMessageBuffers
-import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
@@ -38,42 +34,28 @@
 
 /** Controller for the default flex clock */
 class FlexClockController(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val settings: ClockSettings,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     val design: ClockDesign, // TODO(b/364680879): Remove when done inlining
-    val messageBuffers: ClockMessageBuffers?,
 ) : ClockController {
-    override val smallClock = run {
-        val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+    override val smallClock =
         FlexClockFaceController(
-            ctx,
-            resources,
-            assets.copy(messageBuffer = buffer),
+            clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer),
             design.small ?: design.large!!,
-            false,
-            buffer,
+            isLargeClock = false,
         )
-    }
 
-    override val largeClock = run {
-        val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+    override val largeClock =
         FlexClockFaceController(
-            ctx,
-            resources,
-            assets.copy(messageBuffer = buffer),
+            clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer),
             design.large ?: design.small!!,
-            true,
-            buffer,
+            isLargeClock = true,
         )
-    }
 
     override val config: ClockConfig by lazy {
         ClockConfig(
             DEFAULT_CLOCK_ID,
-            resources.getString(R.string.clock_default_name),
-            resources.getString(R.string.clock_default_description),
+            clockCtx.resources.getString(R.string.clock_default_name),
+            clockCtx.resources.getString(R.string.clock_default_description),
             isReactiveToTone = true,
         )
     }
@@ -125,8 +107,8 @@
         }
 
     override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) {
-        val theme = ThemeConfig(isDarkTheme, assets.seedColor)
-        events.onFontAxesChanged(settings.axes)
+        val theme = ThemeConfig(isDarkTheme, clockCtx.settings.seedColor)
+        events.onFontAxesChanged(clockCtx.settings.axes)
 
         smallClock.run {
             events.onThemeChanged(theme)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index ee21ea6..a8890e6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -16,15 +16,12 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.widget.FrameLayout
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -45,22 +42,19 @@
 
 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController
 class FlexClockFaceController(
-    ctx: Context,
-    private val resources: Resources,
-    val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    clockCtx: ClockContext,
     face: ClockFace,
     private val isLargeClock: Boolean,
-    messageBuffer: MessageBuffer,
 ) : ClockFaceController {
     override val view: View
         get() = layerController.view
 
     override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
 
-    override var theme = ThemeConfig(true, assets.seedColor)
+    override var theme = ThemeConfig(true, clockCtx.settings.seedColor)
 
     private val keyguardLargeClockTopMargin =
-        resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
+        clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
     val layerController: SimpleClockLayerController
     val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
 
@@ -72,23 +66,10 @@
 
         layerController =
             if (isLargeClock) {
-                ComposedDigitalLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    layer as ComposedDigitalHandLayer,
-                    messageBuffer,
-                )
+                ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
             } else {
-                val childView = SimpleDigitalClockTextView(ctx, messageBuffer)
-                SimpleDigitalHandLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    layer as DigitalHandLayer,
-                    childView,
-                    messageBuffer,
-                )
+                val childView = SimpleDigitalClockTextView(clockCtx)
+                SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
             }
         layerController.view.layoutParams = lp
     }
@@ -97,7 +78,7 @@
     private fun offsetGlyphsForStepClockAnimation(
         clockStartLeft: Int,
         direction: Int,
-        fraction: Float
+        fraction: Float,
     ) {
         (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation(
             clockStartLeft,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index 143b28f..ebac4b24 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import android.view.View
 import android.view.ViewGroup
@@ -25,7 +23,6 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -42,14 +39,11 @@
 private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
 
 open class SimpleDigitalHandLayerController<T>(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     private val layer: DigitalHandLayer,
     override val view: T,
-    messageBuffer: MessageBuffer,
 ) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
-    private val logger = Logger(messageBuffer, TAG)
+    private val logger = Logger(clockCtx.messageBuffer, TAG)
     val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
 
     @VisibleForTesting
@@ -75,12 +69,12 @@
             layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
             layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
         }
-        view.applyStyles(assets, layer.style, layer.aodStyle)
+        view.applyStyles(layer.style, layer.aodStyle)
         view.id =
-            ctx.resources.getIdentifier(
+            clockCtx.resources.getIdentifier(
                 generateDigitalLayerIdString(layer),
                 "id",
-                ctx.getPackageName(),
+                clockCtx.context.getPackageName(),
             )
     }
 
@@ -306,8 +300,9 @@
                 val color =
                     when {
                         theme.seedColor != null -> theme.seedColor!!
-                        theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100)
-                        else -> resources.getColor(android.R.color.system_accent2_600)
+                        theme.isDarkTheme ->
+                            clockCtx.resources.getColor(android.R.color.system_accent1_100)
+                        else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
                     }
 
                 view.updateColor(color)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
index b09332f..d4eb767 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -16,24 +16,23 @@
 
 package com.android.systemui.shared.clocks.view
 
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
 import android.view.View
 import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.LogUtil
 import java.util.Locale
 
 // TODO(b/364680879): Merge w/ only subclass FlexClockView
-abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) {
-    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
+    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
 
     abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 593eba9..27ed099 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shared.clocks.view
 
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
 import android.util.MathUtils.constrainedMap
@@ -25,8 +24,7 @@
 import android.widget.RelativeLayout
 import com.android.app.animation.Interpolators
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.MessageBuffer
-import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
 import kotlin.math.abs
 import kotlin.math.max
@@ -34,8 +32,7 @@
 
 fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
 
-class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: MessageBuffer) :
-    DigitalClockFaceView(context, messageBuffer) {
+class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
     override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
     val digitLeftTopMap = mutableMapOf<Int, Point>()
     var maxSingleDigitSize = Point(-1, -1)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 5c84f2d..bd56dbf 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shared.clocks.view
 
 import android.annotation.SuppressLint
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
@@ -37,13 +36,11 @@
 import android.widget.TextView
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.TextAnimator
-import com.android.systemui.animation.TypefaceVariantCache
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.ClockAnimation
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
 import com.android.systemui.shared.clocks.DimensionParser
 import com.android.systemui.shared.clocks.FontTextStyle
@@ -57,18 +54,15 @@
 private val TAG = SimpleDigitalClockTextView::class.simpleName!!
 
 @SuppressLint("AppCompatCustomView")
-open class SimpleDigitalClockTextView(
-    ctx: Context,
-    messageBuffer: MessageBuffer,
-    attrs: AttributeSet? = null,
-) : TextView(ctx, attrs), SimpleDigitalClockView {
+open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
+    TextView(clockCtx.context, attrs), SimpleDigitalClockView {
     val lockScreenPaint = TextPaint()
     override lateinit var textStyle: FontTextStyle
     lateinit var aodStyle: FontTextStyle
 
     private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
     private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION)
-    private val parser = DimensionParser(ctx)
+    private val parser = DimensionParser(clockCtx.context)
     var maxSingleDigitHeight = -1
     var maxSingleDigitWidth = -1
     var digitTranslateAnimator: DigitTranslateAnimator? = null
@@ -91,29 +85,19 @@
     private val prevTextBounds = Rect()
     // targetTextBounds holds the state we are interpolating to
     private val targetTextBounds = Rect()
-    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
 
     private var aodDozingInterpolator: Interpolator? = null
 
     @VisibleForTesting lateinit var textAnimator: TextAnimator
 
-    lateinit var typefaceCache: TypefaceVariantCache
-        private set
-
-    private fun setTypefaceCache(value: TypefaceVariantCache) {
-        typefaceCache = value
-        if (this::textAnimator.isInitialized) {
-            textAnimator.typefaceCache = value
-        }
-    }
+    private val typefaceCache = clockCtx.typefaceCache.getVariantCache("")
 
     @VisibleForTesting
     var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
         TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also {
-            if (this::typefaceCache.isInitialized) {
-                it.typefaceCache = typefaceCache
-            }
+            it.typefaceCache = typefaceCache
         }
     }
 
@@ -436,10 +420,9 @@
         return updateXtranslation(localTranslation, interpolatedTextBounds)
     }
 
-    override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) {
+    override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) {
         this.textStyle = textStyle as FontTextStyle
         val typefaceName = "fonts/" + textStyle.fontFamily
-        setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName))
         lockScreenPaint.strokeJoin = Paint.Join.ROUND
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
         textStyle.fontFeatureSettings?.let {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
index 3d2ed4a1..e8be28f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.TextStyle
 
 interface SimpleDigitalClockView {
@@ -29,7 +28,7 @@
     val textStyle: TextStyle
     @VisibleForTesting var isAnimationEnabled: Boolean
 
-    fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?)
+    fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?)
 
     fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
index 57a6797..85bdf92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.navigationbar.NavigationBarController
 import com.android.systemui.settings.FakeDisplayTracker
-import com.android.systemui.shade.data.repository.FakeShadePositionRepository
+import com.android.systemui.shade.data.repository.FakeShadeDisplayRepository
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,7 +62,7 @@
         mock(ConnectedDisplayKeyguardPresentation::class.java)
     @Mock private val deviceStateHelper = mock(DeviceStateHelper::class.java)
     @Mock private val keyguardStateController = mock(KeyguardStateController::class.java)
-    private val shadePositionRepository = FakeShadePositionRepository()
+    private val shadePositionRepository = FakeShadeDisplayRepository()
 
     private val mainExecutor = Executor { it.run() }
     private val backgroundExecutor = Executor { it.run() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 72163e4..f82c8b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -31,7 +31,8 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.whenever
@@ -62,7 +63,7 @@
     private val testScope = kosmos.testScope
     private val clock = FakeSystemClock()
     private val userRepository = FakeUserRepository()
-    private val mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository
+    private val mobileConnectionsRepository = kosmos.mobileConnectionsRepository
 
     private lateinit var underTest: AuthenticationRepository
 
@@ -110,7 +111,7 @@
                 .isEqualTo(AuthenticationMethodModel.None)
 
             currentSecurityMode = KeyguardSecurityModel.SecurityMode.SimPin
-            mobileConnectionsRepository.isAnySimSecure.value = true
+            mobileConnectionsRepository.fake.isAnySimSecure.value = true
             assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Sim)
             assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Sim)
         }
@@ -199,7 +200,7 @@
         }
 
     private fun setSecurityModeAndDispatchBroadcast(
-        securityMode: KeyguardSecurityModel.SecurityMode,
+        securityMode: KeyguardSecurityModel.SecurityMode
     ) {
         currentSecurityMode = securityMode
         dispatchBroadcast()
@@ -208,23 +209,15 @@
     private fun dispatchBroadcast() {
         fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
             context,
-            Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)
+            Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
         )
     }
 
     companion object {
         private val USER_INFOS =
             listOf(
-                UserInfo(
-                    /* id= */ 100,
-                    /* name= */ "First user",
-                    /* flags= */ 0,
-                ),
-                UserInfo(
-                    /* id= */ 101,
-                    /* name= */ "Second user",
-                    /* flags= */ 0,
-                ),
+                UserInfo(/* id= */ 100, /* name= */ "First user", /* flags= */ 0),
+                UserInfo(/* id= */ 101, /* name= */ "Second user", /* flags= */ 0),
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
index 566cd70..10bf523 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
@@ -35,7 +35,8 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
 import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -78,7 +79,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository
+        mobileConnectionsRepository = kosmos.mobileConnectionsRepository.fake
 
         overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL)
         overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL)
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 8062358..a65e7ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -19,7 +19,9 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.os.RemoteException
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import android.service.dreams.Flags
 import android.service.dreams.IDreamOverlay
 import android.service.dreams.IDreamOverlayCallback
@@ -33,7 +35,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.app.viewcapture.ViewCapture
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager
@@ -43,6 +44,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
@@ -62,12 +64,16 @@
 import com.android.systemui.complication.dagger.ComplicationComponent
 import com.android.systemui.dreams.complication.HideComplicationTouchHandler
 import com.android.systemui.dreams.dagger.DreamOverlayComponent
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.gesture.domain.gestureInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
 import com.android.systemui.navigationbar.gestural.domain.TaskInfo
 import com.android.systemui.navigationbar.gestural.domain.TaskMatcher
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.android.systemui.touch.TouchInsetManager
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -98,12 +104,14 @@
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-class DreamOverlayServiceTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
     private val mFakeSystemClock = FakeSystemClock()
     private val mMainExecutor = FakeExecutor(mFakeSystemClock)
     private val kosmos = testKosmos()
@@ -245,6 +253,10 @@
         )
     }
 
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -287,6 +299,7 @@
                 mKeyguardUpdateMonitor,
                 mScrimManager,
                 mCommunalInteractor,
+                kosmos.sceneInteractor,
                 mSystemDialogsCloser,
                 mUiEventLogger,
                 mTouchInsetManager,
@@ -768,6 +781,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     @kotlin.Throws(RemoteException::class)
     fun testTransitionToGlanceableHub() =
         testScope.runTest {
@@ -793,6 +807,35 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_SCENE_CONTAINER, FLAG_COMMUNAL_HUB)
+    @kotlin.Throws(RemoteException::class)
+    fun testTransitionToGlanceableHub_sceneContainer() =
+        testScope.runTest {
+            // Inform the overlay service of dream starting. Do not show dream complications.
+            client.startDream(
+                mWindowParams,
+                mDreamOverlayCallback,
+                DREAM_COMPONENT,
+                false /*isPreview*/,
+                false, /*shouldShowComplication*/
+            )
+            mMainExecutor.runAllReady()
+
+            verify(mDreamOverlayCallback).onRedirectWake(false)
+            clearInvocations(mDreamOverlayCallback)
+            kosmos.setCommunalAvailable(true)
+            mMainExecutor.runAllReady()
+            runCurrent()
+            verify(mDreamOverlayCallback).onRedirectWake(true)
+            client.onWakeRequested()
+            mMainExecutor.runAllReady()
+            runCurrent()
+            assertThat(kosmos.sceneContainerRepository.currentScene.value)
+                .isEqualTo(Scenes.Communal)
+            verify(mUiEventLogger).log(CommunalUiEvent.DREAM_TO_COMMUNAL_HUB_DREAM_AWAKE_START)
+        }
+
+    @Test
     @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
     @Throws(RemoteException::class)
     fun testRedirectExit() =
@@ -911,6 +954,7 @@
     // Verifies that the touch handling lifecycle is STARTED even if the dream starts while not
     // focused.
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun testLifecycle_dreamNotFocusedOnStart_isStarted() {
         val transitionState: MutableStateFlow<ObservableTransitionState> =
             MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank))
@@ -1024,6 +1068,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun testCommunalVisible_setsLifecycleState() {
         val client = client
 
@@ -1060,6 +1105,7 @@
 
     // Verifies the dream's lifecycle
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun testLifecycleStarted_whenAnyOcclusion() {
         val client = client
 
@@ -1256,5 +1302,11 @@
             ComponentName("package", "homeControlPanel")
         private const val DREAM_COMPONENT = "package/dream"
         private const val WINDOW_NAME = "test"
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_HUB).andSceneContainer()
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt
index 5efb617..f6a6e54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt
@@ -119,22 +119,6 @@
         }
 
     @Test
-    fun onLongClick_whenTileDoesNotHandleLongClick_playsFailureHaptics() =
-        testScope.runTest {
-            // WHEN the tile is long-clicked but the tile does not handle a long-click
-            val state = QSTile.State().apply { handlesLongClick = false }
-            qsTile.changeState(state)
-            underTest.setTileInteractionState(
-                TileHapticsViewModel.TileInteractionState.LONG_CLICKED
-            )
-            runCurrent()
-
-            // THEN the failure token plays
-            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
-            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
-        }
-
-    @Test
     fun whenLaunchingFromClick_doesNotPlayHaptics() =
         testScope.runTest {
             // WHEN the tile is clicked and its action state changes accordingly
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 4e429c3..69fb03d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -47,7 +47,8 @@
 import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT
 import com.android.systemui.keyguard.shared.model.DevicePosture
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
@@ -99,7 +100,7 @@
     private lateinit var devicePostureRepository: FakeDevicePostureRepository
     private lateinit var facePropertyRepository: FakeFacePropertyRepository
     private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
-    private val mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository
+    private val mobileConnectionsRepository = kosmos.mobileConnectionsRepository
 
     private lateinit var testDispatcher: TestDispatcher
     private lateinit var testScope: TestScope
@@ -142,7 +143,7 @@
             1,
             SensorStrength.STRONG,
             FingerprintSensorType.UDFPS_OPTICAL,
-            emptyMap()
+            emptyMap(),
         )
         verify(lockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture())
         verify(authController, times(2)).addCallback(authControllerCallback.capture())
@@ -247,7 +248,7 @@
     private fun deviceIsInPostureThatSupportsFaceAuth() {
         overrideResource(
             R.integer.config_face_auth_supported_posture,
-            DevicePostureController.DEVICE_POSTURE_FLIPPED
+            DevicePostureController.DEVICE_POSTURE_FLIPPED,
         )
         devicePostureRepository.setCurrentPosture(DevicePosture.FLIPPED)
     }
@@ -459,7 +460,7 @@
 
             biometricsAreEnabledBySettings()
             doNotDisableKeyguardAuthFeatures()
-            mobileConnectionsRepository.isAnySimSecure.value = false
+            mobileConnectionsRepository.fake.isAnySimSecure.value = false
             runCurrent()
 
             val isFaceAuthEnabledAndEnrolled by
@@ -467,7 +468,7 @@
 
             assertThat(isFaceAuthEnabledAndEnrolled).isTrue()
 
-            mobileConnectionsRepository.isAnySimSecure.value = true
+            mobileConnectionsRepository.fake.isAnySimSecure.value = true
             runCurrent()
 
             assertThat(isFaceAuthEnabledAndEnrolled).isFalse()
@@ -485,13 +486,13 @@
             doNotDisableKeyguardAuthFeatures()
             faceAuthIsStrongBiometric()
             biometricsAreEnabledBySettings()
-            mobileConnectionsRepository.isAnySimSecure.value = false
+            mobileConnectionsRepository.fake.isAnySimSecure.value = false
 
             onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
             onNonStrongAuthChanged(false, PRIMARY_USER_ID)
             assertThat(isFaceAuthCurrentlyAllowed).isTrue()
 
-            mobileConnectionsRepository.isAnySimSecure.value = true
+            mobileConnectionsRepository.fake.isAnySimSecure.value = true
             assertThat(isFaceAuthCurrentlyAllowed).isFalse()
         }
 
@@ -584,7 +585,7 @@
         testScope.runTest {
             overrideResource(
                 R.integer.config_face_auth_supported_posture,
-                DevicePostureController.DEVICE_POSTURE_UNKNOWN
+                DevicePostureController.DEVICE_POSTURE_UNKNOWN,
             )
 
             createBiometricSettingsRepository()
@@ -597,7 +598,7 @@
         testScope.runTest {
             overrideResource(
                 R.integer.config_face_auth_supported_posture,
-                DevicePostureController.DEVICE_POSTURE_FLIPPED
+                DevicePostureController.DEVICE_POSTURE_FLIPPED,
             )
 
             createBiometricSettingsRepository()
@@ -749,7 +750,7 @@
                 1,
                 SensorStrength.STRONG,
                 FingerprintSensorType.UDFPS_OPTICAL,
-                emptyMap()
+                emptyMap(),
             )
             // Non strong auth is not allowed now, FP is marked strong
             onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
@@ -761,7 +762,7 @@
                 1,
                 SensorStrength.CONVENIENCE,
                 FingerprintSensorType.UDFPS_OPTICAL,
-                emptyMap()
+                emptyMap(),
             )
             assertThat(isFingerprintCurrentlyAllowed).isFalse()
 
@@ -769,7 +770,7 @@
                 1,
                 SensorStrength.WEAK,
                 FingerprintSensorType.UDFPS_OPTICAL,
-                emptyMap()
+                emptyMap(),
             )
             assertThat(isFingerprintCurrentlyAllowed).isFalse()
         }
@@ -791,7 +792,7 @@
                 1,
                 SensorStrength.STRONG,
                 FingerprintSensorType.UDFPS_OPTICAL,
-                emptyMap()
+                emptyMap(),
             )
             // Non strong auth is not allowed now, FP is marked strong
             onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, PRIMARY_USER_ID)
@@ -830,7 +831,7 @@
             UserInfo(
                 /* id= */ PRIMARY_USER_ID,
                 /* name= */ "primary user",
-                /* flags= */ UserInfo.FLAG_PRIMARY
+                /* flags= */ UserInfo.FLAG_PRIMARY,
             )
 
         private const val ANOTHER_USER_ID = 1
@@ -838,7 +839,7 @@
             UserInfo(
                 /* id= */ ANOTHER_USER_ID,
                 /* name= */ "another user",
-                /* flags= */ UserInfo.FLAG_PRIMARY
+                /* flags= */ UserInfo.FLAG_PRIMARY,
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index b72668b..921a8a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -41,8 +41,8 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.util.animation.DisappearParameters
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
index bf97afe..9590816 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -194,6 +195,24 @@
                 .isFalse()
         }
 
+    @Test
+    fun invisibleDueToOcclusion_isDreaming_emitsTrue() =
+        testScope.runTest {
+            val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
+
+            // Verify that we start with unoccluded
+            assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse()
+
+            // Start dreaming, which is an occluding activity
+            showOccludingActivity()
+            kosmos.keyguardInteractor.setDreaming(true)
+
+            // Verify not invisible when dreaming
+            assertWithMessage("Should be invisible when dreaming")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+        }
+
     /** Simulates the appearance of a show-when-locked `Activity` in the foreground. */
     private fun TestScope.showOccludingActivity() {
         keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
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 af0274b..2e074da 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
@@ -73,6 +73,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.keyguard.dismissCallbackRegistry
+import com.android.systemui.keyguard.domain.interactor.dozeInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor
@@ -143,6 +144,8 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val deviceEntryHapticsInteractor by lazy { kosmos.deviceEntryHapticsInteractor }
+    private val dozeInteractor by lazy { kosmos.dozeInteractor }
+    private val keyguardInteractor by lazy { kosmos.keyguardInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val sceneBackInteractor by lazy { kosmos.sceneBackInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
@@ -373,6 +376,64 @@
         }
 
     @Test
+    fun hydrateVisibility_whileDreaming() =
+        testScope.runTest {
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+
+            // GIVEN the device is dreaming
+            val transitionState =
+                prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Dream)
+            underTest.start()
+            assertThat(isVisible).isFalse()
+        }
+
+    @Test
+    fun hydrateVisibility_onCommunalWhileOccluded() =
+        testScope.runTest {
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+
+            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                true,
+                mock(),
+            )
+            prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Communal)
+            underTest.start()
+            runCurrent()
+            assertThat(isVisible).isTrue()
+        }
+
+    @Test
+    fun hydrateVisibility_inCommunalTransition() =
+        testScope.runTest {
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+
+            // GIVEN the device is dreaming
+            val transitionState =
+                prepareState(
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                    isDeviceUnlocked = false,
+                    initialSceneKey = Scenes.Dream,
+                )
+            underTest.start()
+            assertThat(isVisible).isFalse()
+
+            // WHEN a transition starts to the communal hub
+            sceneInteractor.changeScene(Scenes.Dream, "switching to dream for test")
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Dream,
+                    toScene = Scenes.Communal,
+                    currentScene = flowOf(Scenes.Dream),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+            // THEN scenes are visible
+            assertThat(isVisible).isTrue()
+        }
+
+    @Test
     fun startsInLockscreenScene() =
         testScope.runTest {
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -643,7 +704,7 @@
     fun switchToAOD_whenAvailable_whenDeviceSleepsLocked() =
         testScope.runTest {
             kosmos.lockscreenSceneTransitionInteractor.start()
-            val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+            val asleepState by collectLastValue(keyguardInteractor.asleepKeyguardState)
             val currentTransitionInfo by
                 collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal)
             val transitionState =
@@ -673,7 +734,7 @@
     fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked() =
         testScope.runTest {
             kosmos.lockscreenSceneTransitionInteractor.start()
-            val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+            val asleepState by collectLastValue(keyguardInteractor.asleepKeyguardState)
             val currentTransitionInfo by
                 collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal)
             val transitionState =
@@ -2360,6 +2421,66 @@
         }
 
     @Test
+    fun stayOnLockscreen_whenDozingStarted() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            prepareState()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            underTest.start()
+
+            // Stay on Lockscreen when dozing and dreaming
+            dozeInteractor.setIsDozing(true)
+            keyguardInteractor.setDreaming(true)
+            kosmos.fakeKeyguardRepository.setDreamingWithOverlay(false)
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
+    fun switchFromLockscreenToDream_whenDreamStarted() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            prepareState()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            underTest.start()
+
+            powerInteractor.setAwakeForTest()
+            keyguardInteractor.setDreaming(true)
+            // Move past initial delay with [KeyguardInteractor#isAbleToDream]
+            advanceTimeBy(600L)
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Dream)
+        }
+
+    @Test
+    fun switchFromDreamToLockscreen_whenLockedAndDreamStopped() =
+        testScope.runTest {
+            keyguardInteractor.setDreaming(true)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            prepareState(initialSceneKey = Scenes.Dream)
+            assertThat(currentScene).isEqualTo(Scenes.Dream)
+            underTest.start()
+
+            keyguardInteractor.setDreaming(false)
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
+    fun switchFromDreamToGone_whenUnlockedAndDreamStopped() =
+        testScope.runTest {
+            keyguardInteractor.setDreaming(true)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            prepareState(initialSceneKey = Scenes.Dream, isDeviceUnlocked = true)
+            assertThat(currentScene).isEqualTo(Scenes.Dream)
+            underTest.start()
+
+            keyguardInteractor.setDreaming(false)
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+        }
+
+    @Test
     fun replacesLockscreenSceneOnBackStack_whenUnlockdViaAlternateBouncer_fromShade() =
         testScope.runTest {
             val transitionState =
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 01c17bd..94a19c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -148,7 +148,6 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
@@ -428,7 +427,7 @@
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
                 mKosmos.getDeviceProvisioningInteractor(),
-                new FakeDisableFlagsRepository(),
+                mKosmos.getDisableFlagsInteractor(),
                 mDozeParameters,
                 mFakeKeyguardRepository,
                 mKeyguardTransitionInteractor,
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 443595d..ef132d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -149,7 +149,7 @@
     @Mock protected LargeScreenHeaderHelper mLargeScreenHeaderHelper;
 
     protected FakeDisableFlagsRepository mDisableFlagsRepository =
-            new FakeDisableFlagsRepository();
+            mKosmos.getFakeDisableFlagsRepository();
     protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
     protected FakeShadeRepository mShadeRepository = new FakeShadeRepository();
 
@@ -185,7 +185,7 @@
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
                 mKosmos.getDeviceProvisioningInteractor(),
-                mDisableFlagsRepository,
+                mKosmos.getDisableFlagsInteractor(),
                 mDozeParameters,
                 mKeyguardRepository,
                 keyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
index 46961d4..ee3f801 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
@@ -19,7 +19,7 @@
 import android.app.StatusBarManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
@@ -61,7 +61,7 @@
             mDisableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(
                     StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_QUICK_SETTINGS
+                    StatusBarManager.DISABLE2_QUICK_SETTINGS,
                 )
             runCurrent()
 
@@ -76,7 +76,7 @@
             mDisableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(
                     StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+                    StatusBarManager.DISABLE2_NOTIFICATION_SHADE,
                 )
             runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
similarity index 95%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
index a9a5cac..4e7839e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
@@ -34,13 +34,13 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ShadePositionRepositoryTest : SysuiTestCase() {
+class ShadeDisplaysRepositoryTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val commandRegistry = kosmos.commandRegistry
     private val pw = PrintWriter(StringWriter())
 
-    private val underTest = ShadePositionRepositoryImpl(commandRegistry)
+    private val underTest = ShadeDisplaysRepositoryImpl(commandRegistry)
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index f192b59..8ef1e56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.display.data.repository.FakeDisplayWindowPropertiesRepository
 import com.android.systemui.display.shared.model.DisplayWindowProperties
 import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.data.repository.FakeShadePositionRepository
+import com.android.systemui.shade.data.repository.FakeShadeDisplayRepository
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
@@ -50,7 +50,7 @@
 class ShadeDisplaysInteractorTest : SysuiTestCase() {
 
     private val shadeRootview = mock<WindowRootView>()
-    private val positionRepository = FakeShadePositionRepository()
+    private val positionRepository = FakeShadeDisplayRepository()
     private val defaultContext = mock<Context>()
     private val secondaryContext = mock<Context>()
     private val contextStore = FakeDisplayWindowPropertiesRepository()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 19ac0cf..da652c4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -37,8 +37,8 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.shade.shadeTestUtil
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
index 907c684..cd07821 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
index 79ff4be..7eac7e8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
@@ -21,8 +21,8 @@
 import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -51,10 +51,7 @@
     fun disableFlags_notifAlertsNotDisabled_notifAlertsEnabledTrue() =
         with(testComponent) {
             disableFlags.disableFlags.value =
-                DisableFlagsModel(
-                    StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_NONE,
-                )
+                DisableFlagsModel(StatusBarManager.DISABLE_NONE, StatusBarManager.DISABLE2_NONE)
             assertThat(underTest.areNotificationAlertsEnabled()).isTrue()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index f6d439a..5a77f3d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.os.ParcelUuid
-import android.telephony.SubscriptionManager
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING
 import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
@@ -25,91 +24,78 @@
 import androidx.test.filters.SmallTest
 import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
-import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.flags.fake
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
-import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.data.repository.fake
 import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.util.CarrierConfigTracker
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.util.UUID
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class MobileIconsInteractorTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-
-    private lateinit var underTest: MobileIconsInteractor
-    private lateinit var connectivityRepository: FakeConnectivityRepository
-    private lateinit var connectionsRepository: FakeMobileConnectionsRepository
-    private val userSetupRepository = FakeUserSetupRepository()
-    private val mobileMappingsProxy = FakeMobileMappingsProxy()
-    private val flags =
-        FakeFeatureFlagsClassic().apply {
-            set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+    private val kosmos by lazy {
+        testKosmos().apply {
+            mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest"
+            mobileConnectionsRepository.fake.run {
+                setMobileConnectionRepositoryMap(
+                    mapOf(
+                        SUB_1_ID to CONNECTION_1,
+                        SUB_2_ID to CONNECTION_2,
+                        SUB_3_ID to CONNECTION_3,
+                        SUB_4_ID to CONNECTION_4,
+                    )
+                )
+                setActiveMobileDataSubscriptionId(SUB_1_ID)
+            }
+            featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
         }
+    }
 
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    // shortcut rename
+    private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake }
 
-    private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconsInteractorTest")
+    private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() }
 
-    @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        connectivityRepository = FakeConnectivityRepository()
-
-        connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
-        connectionsRepository.setMobileConnectionRepositoryMap(
-            mapOf(
-                SUB_1_ID to CONNECTION_1,
-                SUB_2_ID to CONNECTION_2,
-                SUB_3_ID to CONNECTION_3,
-                SUB_4_ID to CONNECTION_4,
-            )
+    private val Kosmos.underTest by Fixture {
+        MobileIconsInteractorImpl(
+            mobileConnectionsRepository,
+            carrierConfigTracker,
+            tableLogger = mock(),
+            connectivityRepository,
+            FakeUserSetupRepository(),
+            testScope.backgroundScope,
+            context,
+            featureFlagsClassic,
         )
-        connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
-
-        underTest =
-            MobileIconsInteractorImpl(
-                connectionsRepository,
-                carrierConfigTracker,
-                tableLogger = mock(),
-                connectivityRepository,
-                userSetupRepository,
-                testScope.backgroundScope,
-                context,
-                flags,
-            )
     }
 
     @Test
     fun filteredSubscriptions_default() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
@@ -118,7 +104,7 @@
     // Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
     @Test
     fun filteredSubscriptions_moreThanTwo_doesNotFilter() =
-        testScope.runTest {
+        kosmos.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
 
@@ -129,7 +115,7 @@
 
     @Test
     fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
-        testScope.runTest {
+        kosmos.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
 
             val latest by collectLastValue(underTest.filteredSubscriptions)
@@ -139,7 +125,7 @@
 
     @Test
     fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() =
-        testScope.runTest {
+        kosmos.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
 
@@ -150,7 +136,7 @@
 
     @Test
     fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub1, sub2) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
@@ -167,7 +153,7 @@
 
     @Test
     fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub3, sub4) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
@@ -187,7 +173,7 @@
 
     @Test
     fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub3, sub4) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
@@ -207,7 +193,7 @@
 
     @Test
     fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub1, sub3) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
@@ -228,7 +214,7 @@
 
     @Test
     fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub1, sub3) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
@@ -249,7 +235,7 @@
 
     @Test
     fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub1, sub3) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
@@ -258,7 +244,7 @@
                 )
             connectionsRepository.setSubscriptions(listOf(sub1, sub3))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
-            connectivityRepository.vcnSubId.value = SUB_3_ID
+            kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
@@ -269,7 +255,7 @@
 
     @Test
     fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() =
-        testScope.runTest {
+        kosmos.runTest {
             val (sub1, sub3) =
                 createSubscriptionPair(
                     subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
@@ -278,7 +264,7 @@
                 )
             connectionsRepository.setSubscriptions(listOf(sub1, sub3))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
-            connectivityRepository.vcnSubId.value = SUB_1_ID
+            kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
@@ -289,9 +275,9 @@
 
     @Test
     fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() =
-        testScope.runTest {
+        kosmos.runTest {
             // GIVEN the flag is false
-            flags.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
+            featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
 
             // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING
             val sub1 =
@@ -313,7 +299,7 @@
 
     @Test
     fun filteredSubscriptions_filtersOutProvisioningSubs() =
-        testScope.runTest {
+        kosmos.runTest {
             val sub1 =
                 SubscriptionModel(
                     subscriptionId = SUB_1_ID,
@@ -326,7 +312,7 @@
                     subscriptionId = SUB_2_ID,
                     isOpportunistic = false,
                     carrierName = "Carrier 2",
-                    profileClass = SubscriptionManager.PROFILE_CLASS_PROVISIONING,
+                    profileClass = PROFILE_CLASS_PROVISIONING,
                 )
 
             connectionsRepository.setSubscriptions(listOf(sub1, sub2))
@@ -339,7 +325,7 @@
     /** Note: I'm not sure if this will ever be the case, but we can test it at least */
     @Test
     fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() =
-        testScope.runTest {
+        kosmos.runTest {
             // This is a contrived test case, where the active subId is the one that would
             // also be filtered by opportunistic filtering.
 
@@ -376,7 +362,7 @@
 
     @Test
     fun filteredSubscriptions_groupedPairAndNonProvisioned_groupedFilteringStillHappens() =
-        testScope.runTest {
+        kosmos.runTest {
             // Grouped filtering only happens when the list of subs is length 2. In this case
             // we'll show that filtering of provisioning subs happens before, and thus grouped
             // filtering happens even though the unfiltered list is length 3
@@ -406,7 +392,7 @@
 
     @Test
     fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
-        testScope.runTest {
+        kosmos.runTest {
             val notExclusivelyNonTerrestrialSub =
                 SubscriptionModel(
                     isExclusivelyNonTerrestrial = false,
@@ -424,7 +410,7 @@
 
     @Test
     fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
-        testScope.runTest {
+        kosmos.runTest {
             val exclusivelyNonTerrestrialSub =
                 SubscriptionModel(
                     isExclusivelyNonTerrestrial = true,
@@ -442,7 +428,7 @@
 
     @Test
     fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
-        testScope.runTest {
+        kosmos.runTest {
             val exclusivelyNonTerrestrialSub =
                 SubscriptionModel(
                     isExclusivelyNonTerrestrial = true,
@@ -476,7 +462,7 @@
 
     @Test
     fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
-        testScope.runTest {
+        kosmos.runTest {
             // Exclusively non-terrestrial sub
             val exclusivelyNonTerrestrialSub =
                 SubscriptionModel(
@@ -507,7 +493,7 @@
 
     @Test
     fun activeDataConnection_turnedOn() =
-        testScope.runTest {
+        kosmos.runTest {
             CONNECTION_1.setDataEnabled(true)
 
             val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
@@ -517,7 +503,7 @@
 
     @Test
     fun activeDataConnection_turnedOff() =
-        testScope.runTest {
+        kosmos.runTest {
             CONNECTION_1.setDataEnabled(true)
             val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
@@ -528,7 +514,7 @@
 
     @Test
     fun activeDataConnection_invalidSubId() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
             connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
@@ -539,7 +525,7 @@
 
     @Test
     fun failedConnection_default_validated_notFailed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -550,7 +536,7 @@
 
     @Test
     fun failedConnection_notDefault_notValidated_notFailed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = false
@@ -561,7 +547,7 @@
 
     @Test
     fun failedConnection_default_notValidated_failed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -572,7 +558,7 @@
 
     @Test
     fun failedConnection_carrierMergedDefault_notValidated_failed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.hasCarrierMergedConnection.value = true
@@ -584,7 +570,7 @@
     /** Regression test for b/275076959. */
     @Test
     fun failedConnection_dataSwitchInSameGroup_notFailed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -602,7 +588,7 @@
 
     @Test
     fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -618,7 +604,7 @@
 
     @Test
     fun alwaysShowDataRatIcon_configHasTrue() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
 
             val config = MobileMappings.Config()
@@ -630,7 +616,7 @@
 
     @Test
     fun alwaysShowDataRatIcon_configHasFalse() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
 
             val config = MobileMappings.Config()
@@ -642,7 +628,7 @@
 
     @Test
     fun alwaysUseCdmaLevel_configHasTrue() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
 
             val config = MobileMappings.Config()
@@ -654,7 +640,7 @@
 
     @Test
     fun alwaysUseCdmaLevel_configHasFalse() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
 
             val config = MobileMappings.Config()
@@ -666,7 +652,7 @@
 
     @Test
     fun isSingleCarrier_zeroSubscriptions_false() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(emptyList())
@@ -676,7 +662,7 @@
 
     @Test
     fun isSingleCarrier_oneSubscription_true() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1))
@@ -686,7 +672,7 @@
 
     @Test
     fun isSingleCarrier_twoSubscriptions_false() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
@@ -696,7 +682,7 @@
 
     @Test
     fun isSingleCarrier_updates() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1))
@@ -708,7 +694,7 @@
 
     @Test
     fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = false
@@ -719,7 +705,7 @@
 
     @Test
     fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -731,7 +717,7 @@
     /** Regression test for b/272586234. */
     @Test
     fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = false
@@ -742,7 +728,7 @@
 
     @Test
     fun mobileIsDefault_updatesWhenRepoUpdates() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -760,7 +746,7 @@
 
     @Test
     fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -774,17 +760,17 @@
 
             // After 1s, the force validation bit is still present, so the connection is not marked
             // as failed
-            advanceTimeBy(1000)
+            testScope.advanceTimeBy(1000)
             assertThat(latest).isFalse()
 
             // After 2s, the force validation expires so the connection updates to failed
-            advanceTimeBy(1001)
+            testScope.advanceTimeBy(1001)
             assertThat(latest).isTrue()
         }
 
     @Test
     fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
@@ -798,7 +784,7 @@
 
     @Test
     fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             // GIVEN the network starts validated
@@ -819,17 +805,17 @@
             // THEN the forced validation bit is still used...
             assertThat(latest).isFalse()
 
-            advanceTimeBy(1000)
+            testScope.advanceTimeBy(1000)
             assertThat(latest).isFalse()
 
             // ... but expires after 2s
-            advanceTimeBy(1001)
+            testScope.advanceTimeBy(1001)
             assertThat(latest).isTrue()
         }
 
     @Test
     fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDefaultConnectionFailed)
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
@@ -837,7 +823,7 @@
 
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
 
-            advanceTimeBy(1000)
+            testScope.advanceTimeBy(1000)
 
             // WHEN another change in same group event happens
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
@@ -847,37 +833,37 @@
             // THEN the forced validation remains for exactly 2 more seconds from now
 
             // 1.500s from second event
-            advanceTimeBy(1500)
+            testScope.advanceTimeBy(1500)
             assertThat(latest).isFalse()
 
             // 2.001s from the second event
-            advanceTimeBy(501)
+            testScope.advanceTimeBy(501)
             assertThat(latest).isTrue()
         }
 
     @Test
     fun isForceHidden_repoHasMobileHidden_true() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isForceHidden)
 
-            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+            kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
 
             assertThat(latest).isTrue()
         }
 
     @Test
     fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isForceHidden)
 
-            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
+            kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
 
             assertThat(latest).isFalse()
         }
 
     @Test
     fun iconInteractor_cachedPerSubId() =
-        testScope.runTest {
+        kosmos.runTest {
             val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
             val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
 
@@ -887,7 +873,7 @@
 
     @Test
     fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() =
-        testScope.runTest {
+        kosmos.runTest {
             val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode)
 
             connectionsRepository.isDeviceEmergencyCallCapable.value = true
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
index 46f822a..db24d4b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
@@ -21,18 +21,18 @@
 import android.app.StatusBarManager.DISABLE_NONE
 import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS
 import android.app.StatusBarManager.DISABLE_SYSTEM_INFO
-import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
-import org.junit.runner.RunWith;
+import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index c4d2569..b9cdd06 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -58,8 +58,8 @@
 import com.android.systemui.statusbar.data.model.StatusBarMode
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 89da465..fb9e96c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -139,7 +139,9 @@
 
     /** Message buffer for large clock rendering */
     val largeClockMessageBuffer: MessageBuffer,
-)
+) {
+    constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
+}
 
 data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
 
diff --git a/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml b/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml
index 7c59aad..71c77a5 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml
@@ -44,13 +44,13 @@
             android:layout_height="wrap_content">
 
             <com.android.systemui.statusbar.notification.row.FooterViewButton
-                android:id="@+id/settings_button"
+                android:id="@+id/history_button"
                 style="@style/TextAppearance.NotificationFooterButtonRedesign"
                 android:layout_width="wrap_content"
                 android:layout_height="48dp"
                 android:background="@drawable/notif_footer_btn_background"
-                android:contentDescription="@string/notification_settings_button_description"
-                android:drawableStart="@drawable/notif_footer_btn_settings"
+                android:contentDescription="@string/notification_history_button_description"
+                android:drawableStart="@drawable/notif_footer_btn_history"
                 android:focusable="true"
                 app:layout_constraintStart_toStartOf="parent" />
 
@@ -64,17 +64,17 @@
                 android:contentDescription="@string/accessibility_clear_all"
                 android:focusable="true"
                 android:text="@string/clear_all_notifications_text"
-                app:layout_constraintEnd_toStartOf="@id/history_button"
-                app:layout_constraintStart_toEndOf="@id/settings_button" />
+                app:layout_constraintEnd_toStartOf="@id/settings_button"
+                app:layout_constraintStart_toEndOf="@id/history_button" />
 
             <com.android.systemui.statusbar.notification.row.FooterViewButton
-                android:id="@+id/history_button"
+                android:id="@+id/settings_button"
                 style="@style/TextAppearance.NotificationFooterButtonRedesign"
                 android:layout_width="wrap_content"
                 android:layout_height="48dp"
                 android:background="@drawable/notif_footer_btn_background"
-                android:contentDescription="@string/notification_history_button_description"
-                android:drawableStart="@drawable/notif_footer_btn_history"
+                android:contentDescription="@string/notification_settings_button_description"
+                android:drawableStart="@drawable/notif_footer_btn_settings"
                 android:focusable="true"
                 app:layout_constraintEnd_toEndOf="parent" />
         </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 95830b5..add459b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -44,7 +44,7 @@
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.views.NavigationBarView;
 import com.android.systemui.settings.DisplayTracker;
-import com.android.systemui.shade.data.repository.ShadePositionRepository;
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository;
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -66,7 +66,7 @@
     private final DisplayManager mDisplayService;
     private final DisplayTracker mDisplayTracker;
     private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
-    private final Provider<ShadePositionRepository> mShadePositionRepositoryProvider;
+    private final Provider<ShadeDisplaysRepository> mShadePositionRepositoryProvider;
     private final ConnectedDisplayKeyguardPresentation.Factory
             mConnectedDisplayKeyguardPresentationFactory;
     private final Context mContext;
@@ -112,7 +112,7 @@
             KeyguardStateController keyguardStateController,
             ConnectedDisplayKeyguardPresentation.Factory
                     connectedDisplayKeyguardPresentationFactory,
-            Provider<ShadePositionRepository> shadePositionRepositoryProvider,
+            Provider<ShadeDisplaysRepository> shadePositionRepositoryProvider,
             @Application CoroutineScope appScope) {
         mContext = context;
         mNavigationBarControllerLazy = navigationBarControllerLazy;
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 b74ca03..35eed5e 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
@@ -35,6 +35,7 @@
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
@@ -363,6 +364,9 @@
         private val interactor: DeviceUnlockedInteractor,
     ) : CoreStartable {
         override fun start() {
+            if (!SceneContainerFlag.isEnabled)
+                return
+
             applicationScope.launch { interactor.activate() }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 1ffbbd2..b330ba3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,6 +65,9 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.shared.model.Scenes;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -162,6 +165,7 @@
 
     private TouchMonitor mTouchMonitor;
 
+    private final SceneInteractor mSceneInteractor;
     private final CommunalInteractor mCommunalInteractor;
 
     private boolean mCommunalAvailable;
@@ -378,6 +382,7 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             ScrimManager scrimManager,
             CommunalInteractor communalInteractor,
+            SceneInteractor sceneInteractor,
             SystemDialogsCloser systemDialogsCloser,
             UiEventLogger uiEventLogger,
             @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager,
@@ -405,6 +410,7 @@
         mDreamOverlayCallbackController = dreamOverlayCallbackController;
         mWindowTitle = windowTitle;
         mCommunalInteractor = communalInteractor;
+        mSceneInteractor = sceneInteractor;
         mSystemDialogsCloser = systemDialogsCloser;
         mGestureInteractor = gestureInteractor;
         mDreamOverlayComponentFactory = dreamOverlayComponentFactory;
@@ -551,9 +557,15 @@
     @Override
     public void onWakeRequested() {
         mUiEventLogger.log(CommunalUiEvent.DREAM_TO_COMMUNAL_HUB_DREAM_AWAKE_START);
-        mCommunalInteractor.changeScene(CommunalScenes.Communal,
-                "dream wake requested",
-                null);
+        if (SceneContainerFlag.isEnabled()) {
+            // Scene interactor can only be modified on main thread.
+            mExecutor.execute(() -> mSceneInteractor.changeScene(Scenes.Communal,
+                    "dream wake redirect to communal"));
+        } else {
+            mCommunalInteractor.changeScene(CommunalScenes.Communal,
+                    "dream wake requested",
+                    null);
+        }
     }
 
     private void updateGestureBlockingLocked() {
@@ -617,7 +629,13 @@
         mSystemDialogsCloser.closeSystemDialogs();
 
         // Hide glanceable hub (this is a nop if glanceable hub is not open).
-        mCommunalInteractor.changeScene(CommunalScenes.Blank, "dream come to front", null);
+        if (SceneContainerFlag.isEnabled()) {
+            // Scene interactor can only be modified on main thread.
+            mExecutor.execute(
+                    () -> mSceneInteractor.changeScene(Scenes.Dream, "closing hub to go to dream"));
+        } else {
+            mCommunalInteractor.changeScene(CommunalScenes.Blank, "dream come to front", null);
+        }
     }
 
     /**
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 b8ac0d2..5a9e52a 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
@@ -54,6 +54,8 @@
                         shadeInteractor.shadeMode,
                     ) { isDeviceUnlocked, shadeMode ->
                         buildList {
+                                add(Swipe.Start to Scenes.Communal)
+
                                 val bouncerOrGone =
                                     if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
                                 add(Swipe.Up to bouncerOrGone)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
index ed7d182..316964a 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
@@ -83,9 +83,6 @@
                     interactionState == TileInteractionState.LONG_CLICKED &&
                         animationState == TileAnimationState.ACTIVITY_LAUNCH ->
                         TileHapticsState.LONG_PRESS
-                    interactionState == TileInteractionState.LONG_CLICKED &&
-                        !tileViewModel.currentState.handlesLongClick ->
-                        TileHapticsState.FAILED_LONGPRESS
                     else -> TileHapticsState.NO_HAPTICS
                 }
             }
@@ -102,7 +99,6 @@
                         TileHapticsState.TOGGLE_ON -> MSDLToken.SWITCH_ON
                         TileHapticsState.TOGGLE_OFF -> MSDLToken.SWITCH_OFF
                         TileHapticsState.LONG_PRESS -> MSDLToken.LONG_PRESS
-                        TileHapticsState.FAILED_LONGPRESS -> MSDLToken.FAILURE
                         TileHapticsState.NO_HAPTICS -> null
                     }
                 tokenToPlay?.let {
@@ -154,7 +150,6 @@
         TOGGLE_ON,
         TOGGLE_OFF,
         LONG_PRESS,
-        FAILED_LONGPRESS,
         NO_HAPTICS,
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 9029563..e3de6d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -62,6 +62,7 @@
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.kotlin.emitOnStart
@@ -93,7 +94,7 @@
     private val footerActionsController: FooterActionsController,
     private val sysuiStatusBarStateController: SysuiStatusBarStateController,
     deviceEntryInteractor: DeviceEntryInteractor,
-    disableFlagsRepository: DisableFlagsRepository,
+    DisableFlagsInteractor: DisableFlagsInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
     @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
@@ -182,8 +183,8 @@
     val isQsEnabled by
         hydrator.hydratedStateOf(
             traceName = "isQsEnabled",
-            initialValue = disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
-            source = disableFlagsRepository.disableFlags.map { it.isQuickSettingsEnabled() },
+            initialValue = DisableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(),
+            source = DisableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() },
         )
 
     var isInSplitShade by mutableStateOf(false)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 0e09ad2..dbad602 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.panels.ui.compose.infinitegrid
 
 import android.graphics.drawable.Animatable
+import android.graphics.drawable.AnimatedVectorDrawable
 import android.graphics.drawable.Drawable
 import android.text.TextUtils
 import androidx.compose.animation.animateColorAsState
@@ -228,7 +229,14 @@
                         }
                     }
                 }
-                is Icon.Loaded -> rememberDrawablePainter(loadedDrawable)
+                is Icon.Loaded -> {
+                    LaunchedEffect(loadedDrawable) {
+                        if (loadedDrawable is AnimatedVectorDrawable) {
+                            loadedDrawable.forceAnimationOnUI()
+                        }
+                    }
+                    rememberDrawablePainter(loadedDrawable)
+                }
             }
 
         Image(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 301ab2b..8f6c4e7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -1429,7 +1429,7 @@
     void makeOverlayToast(int stringId) {
         final Resources res = mContext.getResources();
 
-        final SystemUIToast systemUIToast = mToastFactory.createToast(mContext,
+        final SystemUIToast systemUIToast = mToastFactory.createToast(mContext, mContext,
                 res.getString(stringId), mContext.getPackageName(), UserHandle.myUserId(),
                 res.getConfiguration().orientation);
         if (systemUIToast == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index 667827a..c96ea03 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -138,6 +138,7 @@
                 Overlays.QuickSettingsShade -> false
                 Scenes.Bouncer -> false
                 Scenes.Communal -> true
+                Scenes.Dream -> false
                 Scenes.Gone -> true
                 Scenes.Lockscreen -> true
                 Scenes.QuickSettings -> false
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
index 41a3c8a..b89eb5c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import dagger.Binds
@@ -46,6 +47,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     deviceEntryInteractor: DeviceEntryInteractor,
+    keyguardInteractor: KeyguardInteractor,
     keyguardEnabledInteractor: KeyguardEnabledInteractor,
 ) : SceneResolver {
     override val targetFamily: SceneKey = SceneFamilies.Home
@@ -56,6 +58,7 @@
                 deviceEntryInteractor.canSwipeToEnter,
                 deviceEntryInteractor.isDeviceEntered,
                 deviceEntryInteractor.isUnlocked,
+                keyguardInteractor.isDreamingWithOverlay,
                 transform = ::homeScene,
             )
             .stateIn(
@@ -67,7 +70,8 @@
                         canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value,
                         isDeviceEntered = deviceEntryInteractor.isDeviceEntered.value,
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
-                    )
+                        isDreamingWithOverlay = false,
+                    ),
             )
 
     override fun includesScene(scene: SceneKey): Boolean = scene in homeScenes
@@ -77,8 +81,11 @@
         canSwipeToEnter: Boolean?,
         isDeviceEntered: Boolean,
         isUnlocked: Boolean,
+        isDreamingWithOverlay: Boolean,
     ): SceneKey =
         when {
+            // Dream can run even if Keyguard is disabled, thus it has the highest priority here.
+            isDreamingWithOverlay -> Scenes.Dream
             !isKeyguardEnabled -> Scenes.Gone
             canSwipeToEnter == true -> Scenes.Lockscreen
             !isDeviceEntered -> Scenes.Lockscreen
@@ -91,6 +98,9 @@
             setOf(
                 Scenes.Gone,
                 Scenes.Lockscreen,
+                // Dream is a home scene as the dream activity occludes keyguard and can show the
+                // shade on top.
+                Scenes.Dream,
             )
     }
 }
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 daeaaa5..9125d7e 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
@@ -62,6 +62,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.logger.SceneLogger
 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.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -217,7 +218,9 @@
                                 sceneInteractor.transitionState.mapNotNull { state ->
                                     when (state) {
                                         is ObservableTransitionState.Idle -> {
-                                            if (state.currentScene != Scenes.Gone) {
+                                            if (state.currentScene == Scenes.Dream) {
+                                                false to "dream is showing"
+                                            } else if (state.currentScene != Scenes.Gone) {
                                                 true to "scene is not Gone"
                                             } else if (state.currentOverlays.isNotEmpty()) {
                                                 true to "overlay is shown"
@@ -228,21 +231,30 @@
                                         is ObservableTransitionState.Transition -> {
                                             if (state.fromContent == Scenes.Gone) {
                                                 true to "scene transitioning away from Gone"
+                                            } else if (state.fromContent == Scenes.Dream) {
+                                                true to "scene transitioning away from dream"
                                             } else {
                                                 null
                                             }
                                         }
                                     }
                                 },
+                                sceneInteractor.transitionState.map { state ->
+                                    state.isTransitioningFromOrTo(Scenes.Communal) ||
+                                        state.isIdle(Scenes.Communal)
+                                },
                                 headsUpInteractor.isHeadsUpOrAnimatingAway,
                                 occlusionInteractor.invisibleDueToOcclusion,
                                 alternateBouncerInteractor.isVisible,
                             ) {
                                 visibilityForTransitionState,
+                                isCommunalShowing,
                                 isHeadsUpOrAnimatingAway,
                                 invisibleDueToOcclusion,
                                 isAlternateBouncerVisible ->
                                 when {
+                                    isCommunalShowing ->
+                                        true to "on or transitioning to/from communal"
                                     isHeadsUpOrAnimatingAway -> true to "showing a HUN"
                                     isAlternateBouncerVisible -> true to "showing alternate bouncer"
                                     invisibleDueToOcclusion -> false to "invisible due to occlusion"
@@ -266,6 +278,7 @@
         handleSimUnlock()
         handleDeviceUnlockStatus()
         handlePowerState()
+        handleDreamState()
         handleShadeTouchability()
     }
 
@@ -506,6 +519,31 @@
         }
     }
 
+    private fun handleDreamState() {
+        applicationScope.launch {
+            keyguardInteractor.isAbleToDream
+                .sample(sceneInteractor.transitionState, ::Pair)
+                .collect { (isAbleToDream, transitionState) ->
+                    if (transitionState.isIdle(Scenes.Communal)) {
+                        // The dream is automatically started underneath the hub, don't transition
+                        // to dream when this is happening as communal is still visible on top.
+                        return@collect
+                    }
+                    if (isAbleToDream) {
+                        switchToScene(
+                            targetSceneKey = Scenes.Dream,
+                            loggingReason = "dream started",
+                        )
+                    } else {
+                        switchToScene(
+                            targetSceneKey = SceneFamilies.Home,
+                            loggingReason = "dream stopped",
+                        )
+                    }
+                }
+        }
+    }
+
     private fun handleShadeTouchability() {
         applicationScope.launch {
             shadeInteractor.isShadeTouchable
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index 3a90d2b..503d0bf 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -235,8 +235,7 @@
         if (mBrightnessWarningToast.isToastActive()) {
             return;
         }
-        mBrightnessWarningToast.show(mView.getContext(),
-                R.string.quick_settings_brightness_unable_adjust_msg);
+        mBrightnessWarningToast.show(mView.getContext(), resId);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
index dfbdaa6..40260d0 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
@@ -37,11 +37,14 @@
     private var toastView: View? = null
 
     fun show(viewContext: Context, @StringRes resId: Int) {
+        if (isToastActive()) {
+            return
+        }
         val res = viewContext.resources
         // Show the brightness warning toast with passing the toast inflation required context,
         // userId and resId from SystemUI package.
         val systemUIToast = toastFactory.createToast(
-            viewContext,
+            viewContext, viewContext,
             res.getString(resId), viewContext.packageName, viewContext.getUserId(),
             res.configuration.orientation
         )
@@ -79,13 +82,15 @@
         val inAnimator = systemUIToast.inAnimation
         inAnimator?.start()
 
-        toastView!!.postDelayed({
+        toastView?.postDelayed({
             val outAnimator = systemUIToast.outAnimation
             if (outAnimator != null) {
                 outAnimator.start()
                 outAnimator.addListener(object : AnimatorListenerAdapter() {
                     override fun onAnimationEnd(animator: Animator) {
-                        windowManager.removeViewImmediate(toastView)
+                        if (isToastActive()) {
+                            windowManager.removeViewImmediate(toastView)
+                        }
                         toastView = null
                     }
                 })
@@ -94,7 +99,7 @@
     }
 
     fun isToastActive(): Boolean {
-        return toastView != null && toastView!!.isAttachedToWindow
+        return toastView?.isAttachedToWindow == true
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index e15830e..fed4a26 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
-import com.android.systemui.shade.data.repository.ShadePositionRepository
-import com.android.systemui.shade.data.repository.ShadePositionRepositoryImpl
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
@@ -157,16 +157,16 @@
 
     @SysUISingleton
     @Provides
-    fun provideShadePositionRepository(impl: ShadePositionRepositoryImpl): ShadePositionRepository {
+    fun provideShadePositionRepository(impl: ShadeDisplaysRepositoryImpl): ShadeDisplaysRepository {
         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
         return impl
     }
 
     @Provides
     @IntoMap
-    @ClassKey(ShadePositionRepositoryImpl::class)
+    @ClassKey(ShadeDisplaysRepositoryImpl::class)
     fun provideShadePositionRepositoryAsCoreStartable(
-        impl: ShadePositionRepositoryImpl
+        impl: ShadeDisplaysRepositoryImpl
     ): CoreStartable {
         return if (ShadeWindowGoesAround.isEnabled) {
             impl
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
index 802fc0e..506b4e9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.shade
 
 import android.view.Display
-import com.android.systemui.shade.data.repository.ShadePositionRepository
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.statusbar.commandline.Command
 import java.io.PrintWriter
 
-class ShadePrimaryDisplayCommand(private val positionRepository: ShadePositionRepository) :
+class ShadePrimaryDisplayCommand(private val positionRepository: ShadeDisplaysRepository) :
     Command {
 
     override fun execute(pw: PrintWriter, args: List<String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt
rename to packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
index 37210b9..71c5658 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
@@ -20,7 +20,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
-class FakeShadePositionRepository : ShadePositionRepository {
+class FakeShadeDisplayRepository : ShadeDisplaysRepository {
     private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
 
     override fun setDisplayId(displayId: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt
rename to packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index 24c067a..e920aba 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -25,7 +25,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
-interface ShadePositionRepository {
+interface ShadeDisplaysRepository {
     /** ID of the display which currently hosts the shade */
     val displayId: StateFlow<Int>
 
@@ -41,9 +41,9 @@
 
 /** Source of truth for the display currently holding the shade. */
 @SysUISingleton
-class ShadePositionRepositoryImpl
+class ShadeDisplaysRepositoryImpl
 @Inject
-constructor(private val commandRegistry: CommandRegistry) : ShadePositionRepository, CoreStartable {
+constructor(private val commandRegistry: CommandRegistry) : ShadeDisplaysRepository, CoreStartable {
     private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
 
     override val displayId: StateFlow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index 4e7898d..1055dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.shade.ShadeWindowLayoutParams
-import com.android.systemui.shade.data.repository.ShadePositionRepository
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
 import javax.inject.Inject
@@ -38,13 +38,13 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.withContext
 
-/** Handles Shade window display change when [ShadePositionRepository.displayId] changes. */
+/** Handles Shade window display change when [ShadeDisplaysRepository.displayId] changes. */
 @SysUISingleton
 class ShadeDisplaysInteractor
 @Inject
 constructor(
     private val shadeRootView: WindowRootView,
-    private val shadePositionRepository: ShadePositionRepository,
+    private val shadePositionRepository: ShadeDisplaysRepository,
     @ShadeDisplayAware private val shadeContext: Context,
     private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
     @Background private val bgScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 460bfbb..a653ca2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
@@ -47,7 +47,7 @@
 constructor(
     @Application val scope: CoroutineScope,
     deviceProvisioningInteractor: DeviceProvisioningInteractor,
-    disableFlagsRepository: DisableFlagsRepository,
+    disableFlagsInteractor: DisableFlagsInteractor,
     dozeParams: DozeParameters,
     keyguardRepository: KeyguardRepository,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
@@ -61,13 +61,13 @@
     BaseShadeInteractor by baseShadeInteractor,
     ShadeModeInteractor by shadeModeInteractor {
     override val isShadeEnabled: StateFlow<Boolean> =
-        disableFlagsRepository.disableFlags
+        disableFlagsInteractor.disableFlags
             .map { it.isShadeEnabled() }
             .flowName("isShadeEnabled")
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
 
     override val isQsEnabled: StateFlow<Boolean> =
-        disableFlagsRepository.disableFlags
+        disableFlagsInteractor.disableFlags
             .map { it.isQuickSettingsEnabled() }
             .flowName("isQsEnabled")
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
@@ -114,7 +114,7 @@
 
     override val isExpandToQsEnabled: Flow<Boolean> =
         combine(
-            disableFlagsRepository.disableFlags,
+            disableFlagsInteractor.disableFlags,
             isShadeEnabled,
             keyguardRepository.isDozing,
             userSetupRepository.isUserSetUp,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index da04f6e..b2ca33a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -705,6 +705,7 @@
         final boolean onBouncer = currentScene.equals(Scenes.Bouncer);
         final boolean onCommunal = currentScene.equals(Scenes.Communal);
         final boolean onGone = currentScene.equals(Scenes.Gone);
+        final boolean onDream = currentScene.equals(Scenes.Dream);
         final boolean onLockscreen = currentScene.equals(Scenes.Lockscreen);
         final boolean onQuickSettings = currentScene.equals(Scenes.QuickSettings);
         final boolean onShade = currentScene.equals(Scenes.Shade);
@@ -765,6 +766,8 @@
             // We get here if deviceUnlockStatus.isUnlocked is false but we are no longer on or over
             // a keyguardish scene; we want to return SHADE_LOCKED until isUnlocked is also true.
             newState = StatusBarState.SHADE_LOCKED;
+        } else if (onDream) {
+            newState = StatusBarState.SHADE_LOCKED;
         } else {
             throw new IllegalArgumentException(
                     "unhandled input to calculateStateFromSceneFramework: " + inputLogString);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
index 9004e5d..aeeb042 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.log.dagger.DisableFlagsRepositoryLog
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt
new file mode 100644
index 0000000..4f1b978
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.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.statusbar.disableflags.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class DisableFlagsInteractor @Inject constructor(repository: DisableFlagsRepository) {
+    /** A model of the disable flags last received from [IStatusBar]. */
+    val disableFlags: StateFlow<DisableFlagsModel> = repository.disableFlags
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
index ce25cf5..6507237 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
@@ -1,18 +1,20 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.disableflags.data.model
+package com.android.systemui.statusbar.disableflags.shared.model
 
 import android.app.StatusBarManager.DISABLE2_NONE
 import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
index 8079ce5..4ea597a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
@@ -17,17 +17,15 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import javax.inject.Inject
 
 /** Interactor for notification alerting. */
 @SysUISingleton
 class NotificationAlertsInteractor
 @Inject
-constructor(
-    private val disableFlagsRepository: DisableFlagsRepository,
-) {
+constructor(private val disableFlagsInteractor: DisableFlagsInteractor) {
     /** Returns true if notification alerts are allowed. */
     fun areNotificationAlertsEnabled(): Boolean =
-        disableFlagsRepository.disableFlags.value.areNotificationAlertsEnabled()
+        disableFlagsInteractor.disableFlags.value.areNotificationAlertsEnabled()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
index 9164da7..b2a0272 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.shared.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.model.StatusBarDisableFlagsVisibilityModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -30,13 +30,13 @@
 @SysUISingleton
 class CollapsedStatusBarInteractor
 @Inject
-constructor(disableFlagsRepository: DisableFlagsRepository) {
+constructor(DisableFlagsInteractor: DisableFlagsInteractor) {
     /**
      * The visibilities of various status bar child views, based only on the information we received
      * from disable flags.
      */
     val visibilityViaDisableFlags: Flow<StatusBarDisableFlagsVisibilityModel> =
-        disableFlagsRepository.disableFlags.map {
+        DisableFlagsInteractor.disableFlags.map {
             StatusBarDisableFlagsVisibilityModel(
                 isClockAllowed = it.isClockEnabled,
                 areNotificationIconsAllowed = it.areNotificationIconsEnabled,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c7b6be3..a1b56d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -112,8 +112,7 @@
             selectedUserContext
                 .flatMapLatest { currentContext
                     -> // flatMapLatest because when selectedUserContext emits a new value, we want
-                    // to
-                    // re-create a whole flow
+                    // to re-create a whole flow
                     callbackFlow {
                             val callback =
                                 object : WifiPickerTracker.WifiPickerTrackerCallback {
@@ -132,20 +131,16 @@
                                                 .map { it.toWifiNetworkModel() }
 
                                         // [WifiPickerTracker.connectedWifiEntry] will return the
-                                        // same
-                                        // instance but with updated internals. For example, when
-                                        // its
-                                        // validation status changes from false to true, the same
-                                        // instance is re-used but with the validated field updated.
+                                        // same instance but with updated internals. For example,
+                                        // when its validation status changes from false to true,
+                                        // the same instance is re-used but with the validated
+                                        // field updated.
                                         //
                                         // Because it's the same instance, the flow won't re-emit
-                                        // the
-                                        // value (even though the internals have changed). So, we
-                                        // need
-                                        // to transform it into our internal model immediately.
-                                        // [toWifiNetworkModel] always returns a new instance, so
-                                        // the
-                                        // flow is guaranteed to emit.
+                                        // the value (even though the internals have changed). So,
+                                        // we need to transform it into our internal model
+                                        // immediately. [toWifiNetworkModel] always returns a new
+                                        // instance, so the flow is guaranteed to emit.
                                         send(
                                             newPrimaryNetwork =
                                                 connectedEntry?.toPrimaryWifiNetworkModel()
@@ -235,20 +230,15 @@
                                             .map { it.toWifiNetworkModel() }
 
                                     // [WifiPickerTracker.connectedWifiEntry] will return the same
-                                    // instance
-                                    // but with updated internals. For example, when its validation
-                                    // status
-                                    // changes from false to true, the same instance is re-used but
-                                    // with the
-                                    // validated field updated.
+                                    // instance but with updated internals. For example, when its
+                                    // validation status changes from false to true, the same
+                                    // instance is re-used but with the validated field updated.
                                     //
                                     // Because it's the same instance, the flow won't re-emit the
-                                    // value
-                                    // (even though the internals have changed). So, we need to
-                                    // transform it
-                                    // into our internal model immediately. [toWifiNetworkModel]
-                                    // always
-                                    // returns a new instance, so the flow is guaranteed to emit.
+                                    // value (even though the internals have changed). So, we need
+                                    // to transform it into our internal model immediately.
+                                    // [toWifiNetworkModel] always returns a new instance, so the
+                                    // flow is guaranteed to emit.
                                     send(
                                         newPrimaryNetwork =
                                             connectedEntry?.toPrimaryWifiNetworkModel()
@@ -292,12 +282,10 @@
                                 .create(applicationContext, lifecycle, callback, "WifiRepository")
                                 .apply {
                                     // By default, [WifiPickerTracker] will scan to see all
-                                    // available wifi
-                                    // networks in the area. Because SysUI only needs to display the
-                                    // **connected** network, we don't need scans to be running (and
-                                    // in fact,
-                                    // running scans is costly and should be avoided whenever
-                                    // possible).
+                                    // available wifi networks in the area. Because SysUI only
+                                    // needs to display the **connected** network, we don't
+                                    // need scans to be running (and in fact, running scans is
+                                    // costly and should be avoided whenever possible).
                                     this?.disableScanning()
                                 }
                         // The lifecycle must be STARTED in order for the callback to receive
@@ -382,16 +370,11 @@
 
     private fun MergedCarrierEntry.convertCarrierMergedToModel(): WifiNetworkModel {
         // WifiEntry instance values aren't guaranteed to be stable between method calls
-        // because
-        // WifiPickerTracker is continuously updating the same object. Save the level in a
-        // local
-        // variable so that checking the level validity here guarantees that the level will
-        // still be
-        // valid when we create the `WifiNetworkModel.Active` instance later. Otherwise, the
-        // level
-        // could be valid here but become invalid later, and `WifiNetworkModel.Active` will
-        // throw
-        // an exception. See b/362384551.
+        // because WifiPickerTracker is continuously updating the same object. Save the
+        // level in a local variable so that checking the level validity here guarantees
+        // that the level will still be valid when we create the `WifiNetworkModel.Active`
+        // instance later. Otherwise, the level could be valid here but become invalid
+        // later, and `WifiNetworkModel.Active` will throw an exception. See b/362384551.
 
         return WifiNetworkModel.CarrierMerged.of(
             subscriptionId = this.subscriptionId,
diff --git a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java
index d97cae2..d367455 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java
@@ -50,6 +50,7 @@
 public class SystemUIToast implements ToastPlugin.Toast {
     static final String TAG = "SystemUIToast";
     final Context mContext;
+    final Context mDisplayContext;
     final CharSequence mText;
     final ToastPlugin.Toast mPluginToast;
 
@@ -68,17 +69,18 @@
     @Nullable private final Animator mInAnimator;
     @Nullable private final Animator mOutAnimator;
 
-    SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text,
-            String packageName, int userId, int orientation) {
-        this(layoutInflater, context, text, null, packageName, userId,
+    SystemUIToast(LayoutInflater layoutInflater, Context applicationContext, Context displayContext,
+            CharSequence text, String packageName, int userId, int orientation) {
+        this(layoutInflater, applicationContext, displayContext, text, null, packageName, userId,
                 orientation);
     }
 
-    SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text,
-            ToastPlugin.Toast pluginToast, String packageName, @UserIdInt int userId,
-            int orientation) {
+    SystemUIToast(LayoutInflater layoutInflater, Context applicationContext, Context displayContext,
+            CharSequence text, ToastPlugin.Toast pluginToast, String packageName,
+            @UserIdInt int userId, int orientation) {
         mLayoutInflater = layoutInflater;
-        mContext = context;
+        mContext = applicationContext;
+        mDisplayContext = displayContext;
         mText = text;
         mPluginToast = pluginToast;
         mPackageName = packageName;
@@ -221,9 +223,9 @@
             mPluginToast.onOrientationChange(orientation);
         }
 
-        mDefaultY = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
+        mDefaultY = mDisplayContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
         mDefaultGravity =
-                mContext.getResources().getInteger(R.integer.config_toastDefaultGravity);
+                mDisplayContext.getResources().getInteger(R.integer.config_toastDefaultGravity);
     }
 
     private Animator createInAnimator() {
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java
index 9ae6674..388d4bd 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java
@@ -65,15 +65,16 @@
     /**
      * Create a toast to be shown by ToastUI.
      */
-    public SystemUIToast createToast(Context context, CharSequence text, String packageName,
-            int userId, int orientation) {
-        LayoutInflater layoutInflater = LayoutInflater.from(context);
+    public SystemUIToast createToast(Context applicationContext, Context displayContext,
+            CharSequence text, String packageName, int userId, int orientation) {
+        LayoutInflater layoutInflater = LayoutInflater.from(displayContext);
         if (isPluginAvailable()) {
-            return new SystemUIToast(layoutInflater, context, text, mPlugin.createToast(text,
-                    packageName, userId), packageName, userId, orientation);
+            return new SystemUIToast(layoutInflater, applicationContext, displayContext, text,
+                    mPlugin.createToast(text, packageName, userId), packageName, userId,
+                    orientation);
         }
-        return new SystemUIToast(layoutInflater, context, text, packageName, userId,
-                orientation);
+        return new SystemUIToast(layoutInflater, applicationContext, displayContext, text,
+                packageName, userId, orientation);
     }
 
     private boolean isPluginAvailable() {
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index 32a4f12..12f73b8 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -135,8 +135,8 @@
                 return;
             }
             Context displayContext = context.createDisplayContext(display);
-            mToast = mToastFactory.createToast(displayContext /* sysuiContext */, text, packageName,
-                    userHandle.getIdentifier(), mOrientation);
+            mToast = mToastFactory.createToast(mContext, displayContext /* sysuiContext */, text,
+                    packageName, userHandle.getIdentifier(), mOrientation);
 
             if (mToast.getInAnimation() != null) {
                 mToast.getInAnimation().start();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 65b6273..2aa6e7b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -105,8 +105,7 @@
 
     private val mainExecutor = ImmediateExecutor()
     private lateinit var repository: FakeKeyguardRepository
-    private val messageBuffer = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
-    private val clockBuffers = ClockMessageBuffers(messageBuffer, messageBuffer, messageBuffer)
+    private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG))
     private lateinit var underTest: ClockEventController
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index a940bc9..425aad2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -16,10 +16,12 @@
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
 import android.view.ViewGroup
+import android.view.WindowManager.TRANSIT_NONE
 import android.widget.FrameLayout
 import android.widget.LinearLayout
 import android.window.RemoteTransition
 import android.window.TransitionFilter
+import android.window.WindowAnimationState
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -34,6 +36,10 @@
 import junit.framework.AssertionFailedError
 import kotlin.concurrent.thread
 import kotlin.test.assertEquals
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -258,7 +264,6 @@
     @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
     @Test
     fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() {
-
         val controller =
             object : DelegateTransitionAnimatorController(controller) {
                 override val transitionCookie =
@@ -273,7 +278,6 @@
     @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
     @Test
     fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
-
         // No TransitionCookie
         val controllerWithoutCookie =
             object : DelegateTransitionAnimatorController(controller) {
@@ -348,7 +352,7 @@
     fun doesNotStartIfAnimationIsCancelled() {
         val runner = activityTransitionAnimator.createRunner(controller)
         runner.onAnimationCancelled()
-        runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
+        runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
 
         waitForIdleSync()
         verify(controller).onTransitionAnimationCancelled()
@@ -361,7 +365,7 @@
     @Test
     fun cancelsIfNoOpeningWindowIsFound() {
         val runner = activityTransitionAnimator.createRunner(controller)
-        runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
+        runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
 
         waitForIdleSync()
         verify(controller).onTransitionAnimationCancelled()
@@ -374,7 +378,13 @@
     @Test
     fun startsAnimationIfWindowIsOpening() {
         val runner = activityTransitionAnimator.createRunner(controller)
-        runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback)
+        runner.onAnimationStart(
+            TRANSIT_NONE,
+            arrayOf(fakeWindow()),
+            emptyArray(),
+            emptyArray(),
+            iCallback,
+        )
         waitForIdleSync()
         verify(listener).onTransitionAnimationStart()
         verify(controller).onTransitionAnimationStart(anyBoolean())
@@ -387,6 +397,113 @@
         }
     }
 
+    @DisableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
+    @Test
+    fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() {
+        assertThrows(IllegalStateException::class.java) {
+            activityTransitionAnimator.createRunner(controller, initializeLazily = true)
+        }
+    }
+
+    @EnableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
+    @Test
+    fun runnerCreatesDelegateLazily_whenPostingTimeouts() {
+        val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true)
+        assertNull(runner.delegate)
+        runner.postTimeouts()
+        assertNotNull(runner.delegate)
+    }
+
+    @EnableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
+    @Test
+    fun runnerCreatesDelegateLazily_onAnimationStart() {
+        val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true)
+        assertNull(runner.delegate)
+
+        // The delegate is cleaned up after execution (which happens in another thread), so what we
+        // do instead is check if it becomes non-null at any point with a 1 second timeout. This
+        // will tell us that takeOverWithAnimation() triggered the lazy initialization.
+        var delegateInitialized = false
+        runBlocking {
+            val initChecker = launch {
+                withTimeout(1.seconds) {
+                    while (runner.delegate == null) continue
+                    delegateInitialized = true
+                }
+            }
+            runner.onAnimationStart(
+                TRANSIT_NONE,
+                arrayOf(fakeWindow()),
+                emptyArray(),
+                emptyArray(),
+                iCallback,
+            )
+            initChecker.join()
+        }
+        assertTrue(delegateInitialized)
+    }
+
+    @EnableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
+    @Test
+    fun runnerCreatesDelegateLazily_onAnimationTakeover() {
+        val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true)
+        assertNull(runner.delegate)
+
+        // The delegate is cleaned up after execution (which happens in another thread), so what we
+        // do instead is check if it becomes non-null at any point with a 1 second timeout. This
+        // will tell us that takeOverWithAnimation() triggered the lazy initialization.
+        var delegateInitialized = false
+        runBlocking {
+            val initChecker = launch {
+                withTimeout(1.seconds) {
+                    while (runner.delegate == null) continue
+                    delegateInitialized = true
+                }
+            }
+            runner.takeOverAnimation(
+                arrayOf(fakeWindow()),
+                arrayOf(WindowAnimationState()),
+                SurfaceControl.Transaction(),
+                iCallback,
+            )
+            initChecker.join()
+        }
+        assertTrue(delegateInitialized)
+    }
+
+    @DisableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
+    @Test
+    fun animationTakeoverThrows_whenTheFlagsAreDisabled() {
+        val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = false)
+        assertThrows(IllegalStateException::class.java) {
+            runner.takeOverAnimation(
+                arrayOf(fakeWindow()),
+                emptyArray(),
+                SurfaceControl.Transaction(),
+                iCallback,
+            )
+        }
+    }
+
+    @DisableFlags(
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+    )
     @Test
     fun disposeRunner_delegateDereferenced() {
         val runner = activityTransitionAnimator.createRunner(controller)
@@ -409,7 +526,7 @@
             false,
             Rect(),
             Rect(),
-            0,
+            1,
             Point(),
             Rect(),
             bounds,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index 0b9c06f..5ada2f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -222,7 +222,7 @@
         when(SubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(SUB_ID);
         SubscriptionInfo info = mock(SubscriptionInfo.class);
         when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info);
-        when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt()))
+        when(mToastFactory.createToast(any(), any(), anyString(), anyString(), anyInt(), anyInt()))
             .thenReturn(mSystemUIToast);
         when(mSystemUIToast.getView()).thenReturn(mToastView);
         when(mSystemUIToast.getGravity()).thenReturn(GRAVITY_FLAGS);
@@ -275,8 +275,8 @@
         mInternetDialogController.connectCarrierNetwork();
 
         verify(mMergedCarrierEntry).connect(null /* callback */, false /* showToast */);
-        verify(mToastFactory).createToast(any(), eq(TOAST_MESSAGE_STRING), anyString(), anyInt(),
-            anyInt());
+        verify(mToastFactory).createToast(any(), any(), eq(TOAST_MESSAGE_STRING), anyString(),
+                anyInt(), anyInt());
     }
 
     @Test
@@ -288,7 +288,7 @@
         mInternetDialogController.connectCarrierNetwork();
 
         verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */);
-        verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(),
+        verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(),
             anyInt());
     }
 
@@ -302,7 +302,7 @@
         mInternetDialogController.connectCarrierNetwork();
 
         verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */);
-        verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(),
+        verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(),
             anyInt());
     }
 
@@ -321,7 +321,7 @@
         mInternetDialogController.connectCarrierNetwork();
 
         verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */);
-        verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(),
+        verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(),
             anyInt());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
index 8bd8b72..2812bd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -15,11 +15,16 @@
  */
 package systemui.shared.clocks.view
 
+import android.graphics.Typeface
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
+import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.FontTextStyle
 import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.TypefaceCache
 import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -38,7 +43,23 @@
 
     @Before
     fun setup() {
-        underTest = SimpleDigitalClockTextView(context, messageBuffer)
+        underTest =
+            SimpleDigitalClockTextView(
+                ClockContext(
+                    context,
+                    context.resources,
+                    ClockSettings(),
+                    TypefaceCache(messageBuffer) {
+                        // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
+                        return@TypefaceCache Typeface.create(
+                            "google-sans-flex-clock",
+                            Typeface.NORMAL,
+                        )
+                    },
+                    ClockMessageBuffers(messageBuffer),
+                    messageBuffer,
+                )
+            )
         underTest.textStyle = FontTextStyle()
         underTest.aodStyle = FontTextStyle()
         underTest.text = "0"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index a8618eb..3a46d03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -20,9 +20,8 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -31,7 +30,6 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.statusbar.policy.configurationController
 import com.android.systemui.statusbar.policy.fakeConfigurationController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
@@ -144,7 +142,7 @@
                         context = context,
                         configurationController = configurationController,
                         dumpManager = mock(),
-                        splitShadeStateController = ResourcesSplitShadeStateController()
+                        splitShadeStateController = ResourcesSplitShadeStateController(),
                     ),
                 keyguardTransitionControllerFactory = { notificationPanelController ->
                     LockscreenShadeKeyguardTransitionController(
@@ -153,7 +151,7 @@
                         context = context,
                         configurationController = configurationController,
                         dumpManager = mock(),
-                        splitShadeStateController = ResourcesSplitShadeStateController()
+                        splitShadeStateController = ResourcesSplitShadeStateController(),
                     )
                 },
                 depthController = depthController,
@@ -171,7 +169,7 @@
                 splitShadeStateController = ResourcesSplitShadeStateController(),
                 shadeLockscreenInteractorLazy = { shadeLockscreenInteractor },
                 naturalScrollingSettingObserver = naturalScrollingSettingObserver,
-                lazyQSSceneAdapter = { qsSceneAdapter }
+                lazyQSSceneAdapter = { qsSceneAdapter },
             )
 
         transitionController.addCallback(transitionControllerCallback)
@@ -229,7 +227,7 @@
             verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED)
             assertFalse(
                 "Waking to shade locked when not dozing",
-                transitionController.isWakingToShadeLocked
+                transitionController.isWakingToShadeLocked,
             )
         }
 
@@ -247,9 +245,7 @@
     fun testDontGoWhenShadeDisabled() =
         testScope.runTest {
             disableFlagsRepository.disableFlags.value =
-                DisableFlagsModel(
-                    disable2 = DISABLE2_NOTIFICATION_SHADE,
-                )
+                DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE)
             testScope.runCurrent()
             transitionController.goToLockedShade(null)
             verify(statusbarStateController, never()).setState(anyInt())
@@ -454,7 +450,7 @@
             val distance = 10
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_scrim_transition_distance,
-                distance
+                distance,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -463,7 +459,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = eq(0.5f),
-                    lockScreenNotificationsProgress = anyFloat()
+                    lockScreenNotificationsProgress = anyFloat(),
                 )
         }
 
@@ -474,11 +470,11 @@
             val delay = 10
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -487,7 +483,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(0.1f)
+                    lockScreenNotificationsProgress = eq(0.1f),
                 )
         }
 
@@ -498,11 +494,11 @@
             val delay = 50
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -511,7 +507,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(0f)
+                    lockScreenNotificationsProgress = eq(0f),
                 )
         }
 
@@ -522,11 +518,11 @@
             val delay = 50
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -535,7 +531,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(1f)
+                    lockScreenNotificationsProgress = eq(1f),
                 )
         }
 
@@ -627,7 +623,7 @@
      */
     private fun ScrimController.transitionToFullShadeProgress(
         progress: Float,
-        lockScreenNotificationsProgress: Float
+        lockScreenNotificationsProgress: Float,
     ) {
         setTransitionToFullShadeProgress(progress, lockScreenNotificationsProgress)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 32469b6..ad38bbe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -23,13 +23,13 @@
 
 class FakeFeatureFlagsClassic : FakeFeatureFlags()
 
+val FeatureFlagsClassic.fake
+    get() = this as FakeFeatureFlagsClassic
+
 @Deprecated(
     message = "Use FakeFeatureFlagsClassic instead.",
     replaceWith =
-        ReplaceWith(
-            "FakeFeatureFlagsClassic",
-            "com.android.systemui.flags.FakeFeatureFlagsClassic",
-        ),
+        ReplaceWith("FakeFeatureFlagsClassic", "com.android.systemui.flags.FakeFeatureFlagsClassic"),
 )
 open class FakeFeatureFlags : FeatureFlagsClassic {
     private val booleanFlags = mutableMapOf<String, Boolean>()
@@ -105,6 +105,7 @@
                 listener.onFlagChanged(
                     object : FlagListenable.FlagEvent {
                         override val flagName = flag.name
+
                         override fun requestNoRestart() {}
                     }
                 )
@@ -165,7 +166,7 @@
 
 @Module(includes = [FakeFeatureFlagsClassicModule.Bindings::class])
 class FakeFeatureFlagsClassicModule(
-    @get:Provides val fakeFeatureFlagsClassic: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic(),
+    @get:Provides val fakeFeatureFlagsClassic: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
 ) {
 
     constructor(
@@ -175,7 +176,9 @@
     @Module
     interface Bindings {
         @Binds fun bindFake(fake: FakeFeatureFlagsClassic): FeatureFlagsClassic
+
         @Binds fun bindClassic(classic: FeatureFlagsClassic): FeatureFlags
+
         @Binds fun bindFakeClassic(fake: FakeFeatureFlagsClassic): FakeFeatureFlags
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 3cd613b..3d60abf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -71,6 +71,8 @@
 import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModePerDisplayRepository
+import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
@@ -78,7 +80,7 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.statusbar.phone.keyguardBypassController
 import com.android.systemui.statusbar.phone.scrimController
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.fakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.wifiInteractor
 import com.android.systemui.statusbar.policy.configurationController
@@ -126,7 +128,7 @@
     val keyguardStatusBarViewModel by lazy { kosmos.keyguardStatusBarViewModel }
     val powerRepository by lazy { kosmos.fakePowerRepository }
     val clock by lazy { kosmos.systemClock }
-    val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
+    val mobileConnectionsRepository by lazy { kosmos.mobileConnectionsRepository }
     val simBouncerInteractor by lazy { kosmos.simBouncerInteractor }
     val statusBarStateController by lazy { kosmos.statusBarStateController }
     val statusBarModePerDisplayRepository by lazy { kosmos.fakeStatusBarModePerDisplayRepository }
@@ -183,4 +185,6 @@
     val lockscreenToGlanceableHubTransitionViewModel by lazy {
         kosmos.lockscreenToGlanceableHubTransitionViewModel
     }
+    val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor }
+    val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index 4ed49123..45d5b38 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -33,7 +33,7 @@
 import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.transition.largeScreenShadeInterpolator
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -51,7 +51,7 @@
                     footerActionsController,
                     sysuiStatusBarStateController,
                     deviceEntryInteractor,
-                    disableFlagsRepository,
+                    disableFlagsInteractor,
                     keyguardTransitionInteractor,
                     largeScreenShadeInterpolator,
                     configurationInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
index 8b12425..a4a63ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.scene.shared.model.SceneFamilies
@@ -34,6 +35,7 @@
         HomeSceneFamilyResolver(
             applicationScope = applicationCoroutineScope,
             deviceEntryInteractor = deviceEntryInteractor,
+            keyguardInteractor = keyguardInteractor,
             keyguardEnabledInteractor = keyguardEnabledInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index 39f58ae..af6d624 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.ShadeModule
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
@@ -60,7 +60,7 @@
         ShadeInteractorImpl(
             scope = applicationCoroutineScope,
             deviceProvisioningInteractor = deviceProvisioningInteractor,
-            disableFlagsRepository = disableFlagsRepository,
+            disableFlagsInteractor = disableFlagsInteractor,
             dozeParams = dozeParameters,
             keyguardRepository = fakeKeyguardRepository,
             keyguardTransitionInteractor = keyguardTransitionInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
index 466a3eb..9dbb547 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
@@ -15,7 +15,7 @@
 package com.android.systemui.statusbar.disableflags.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt
new file mode 100644
index 0000000..7b4b047
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.disableflags.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+
+val Kosmos.disableFlagsInteractor by Fixture {
+    DisableFlagsInteractor(repository = disableFlagsRepository)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt
index d76defe..99ed4f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 
 val Kosmos.airplaneModeInteractor: AirplaneModeInteractor by
@@ -26,6 +26,6 @@
         AirplaneModeInteractor(
             FakeAirplaneModeRepository(),
             FakeConnectivityRepository(),
-            fakeMobileConnectionsRepository,
+            mobileConnectionsRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index de73d33..bfd46b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -77,11 +77,7 @@
 
     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
         return subIdRepos[subId]
-            ?: FakeMobileConnectionRepository(
-                    subId,
-                    tableLogBuffer,
-                )
-                .also { subIdRepos[subId] = it }
+            ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it }
     }
 
     override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config())
@@ -135,3 +131,6 @@
         const val LTE_ADVANCED_PRO = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
     }
 }
+
+val MobileConnectionsRepository.fake
+    get() = this as FakeMobileConnectionsRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
index cd22f1d..b952d71 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
@@ -19,12 +19,20 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+val Kosmos.mobileMappingsProxy: MobileMappingsProxy by Fixture { FakeMobileMappingsProxy() }
+
+var Kosmos.mobileConnectionsRepositoryLogbufferName by Fixture { "FakeMobileConnectionsRepository" }
 
 val Kosmos.fakeMobileConnectionsRepository by Fixture {
     FakeMobileConnectionsRepository(
-        tableLogBuffer = logcatTableLogBuffer(this, "FakeMobileConnectionsRepository"),
+        mobileMappings = mobileMappingsProxy,
+        tableLogBuffer = logcatTableLogBuffer(this, mobileConnectionsRepositoryLogbufferName),
     )
 }
 
-val Kosmos.mobileConnectionsRepository by
-    Fixture<MobileConnectionsRepository> { fakeMobileConnectionsRepository }
+val Kosmos.mobileConnectionsRepository: MobileConnectionsRepository by Fixture {
+    fakeMobileConnectionsRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
index 8e656cf..00bfa99 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
@@ -18,7 +18,5 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.fakeConnectivityRepository: FakeConnectivityRepository by
-    Kosmos.Fixture { FakeConnectivityRepository() }
 val Kosmos.connectivityRepository: ConnectivityRepository by
-    Kosmos.Fixture { fakeConnectivityRepository }
+    Kosmos.Fixture { FakeConnectivityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt
index 331e2fa..c69d9a2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt
@@ -42,10 +42,7 @@
      * validated
      */
     @JvmOverloads
-    fun setMobileConnected(
-        default: Boolean = true,
-        validated: Boolean = true,
-    ) {
+    fun setMobileConnected(default: Boolean = true, validated: Boolean = true) {
         defaultConnections.value =
             DefaultConnectionModel(
                 mobile = DefaultConnectionModel.Mobile(default),
@@ -55,10 +52,7 @@
 
     /** Similar convenience method for ethernet */
     @JvmOverloads
-    fun setEthernetConnected(
-        default: Boolean = true,
-        validated: Boolean = true,
-    ) {
+    fun setEthernetConnected(default: Boolean = true, validated: Boolean = true) {
         defaultConnections.value =
             DefaultConnectionModel(
                 ethernet = DefaultConnectionModel.Ethernet(default),
@@ -67,10 +61,7 @@
     }
 
     @JvmOverloads
-    fun setWifiConnected(
-        default: Boolean = true,
-        validated: Boolean = true,
-    ) {
+    fun setWifiConnected(default: Boolean = true, validated: Boolean = true) {
         defaultConnections.value =
             DefaultConnectionModel(
                 wifi = DefaultConnectionModel.Wifi(default),
@@ -78,3 +69,6 @@
             )
     }
 }
+
+val ConnectivityRepository.fake
+    get() = this as FakeConnectivityRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
index 385a813..13fde96 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.shared.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 
 val Kosmos.collapsedStatusBarInteractor: CollapsedStatusBarInteractor by
-    Kosmos.Fixture { CollapsedStatusBarInteractor(fakeDisableFlagsRepository) }
+    Kosmos.Fixture { CollapsedStatusBarInteractor(disableFlagsInteractor) }
diff --git a/ravenwood/scripts/run-ravenwood-tests.sh b/ravenwood/scripts/run-ravenwood-tests.sh
index fe2269a..27c5ea1 100755
--- a/ravenwood/scripts/run-ravenwood-tests.sh
+++ b/ravenwood/scripts/run-ravenwood-tests.sh
@@ -33,7 +33,7 @@
 exclude_re=""
 smoke_exclude_re=""
 dry_run=""
-while getopts "sx:f:dt" opt; do
+while getopts "sx:f:dtb" opt; do
 case "$opt" in
     s)
         # Remove slow tests.
@@ -52,8 +52,13 @@
         dry_run="echo"
         ;;
     t)
+        # Redirect log to terminal
         export RAVENWOOD_LOG_OUT=$(tty)
         ;;
+    b)
+        # Build only
+        ATEST=m
+        ;;
     '?')
         exit 1
         ;;
@@ -99,11 +104,16 @@
 
 # Calculate the removed tests.
 
-diff="$(diff  <(echo "${all_tests[@]}" | tr ' ' '\n') <(echo "${targets[@]}" | tr ' ' '\n') )"
+diff="$(diff  <(echo "${all_tests[@]}" | tr ' ' '\n') <(echo "${targets[@]}" | tr ' ' '\n') | grep -v [0-9] )"
 
 if [[ "$diff" != "" ]]; then
     echo "Excluded tests:"
     echo "$diff"
 fi
 
-$dry_run ${ATEST:-atest} "${targets[@]}"
+run() {
+    echo "Running: ${@}"
+    "${@}"
+}
+
+run $dry_run ${ATEST:-atest} "${targets[@]}"
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index cd2a535..e59bb42 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -28,8 +28,11 @@
 import android.app.assist.AssistStructure;
 import android.app.assist.AssistStructure.ViewNode;
 import android.app.assist.AssistStructure.WindowNode;
+import android.app.slice.Slice;
+import android.app.slice.SliceItem;
 import android.content.ComponentName;
 import android.content.Context;
+import android.graphics.drawable.Icon;
 import android.hardware.display.DisplayManager;
 import android.metrics.LogMaker;
 import android.os.UserHandle;
@@ -97,11 +100,12 @@
             @UserIdInt int userId, @NonNull RemoteViews rView) {
         final AtomicBoolean permissionsOk = new AtomicBoolean(true);
 
-        rView.visitUris(uri -> {
-            int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri);
-            boolean allowed = uriOwnerId == userId;
-            permissionsOk.set(allowed & permissionsOk.get());
-        });
+        rView.visitUris(
+                uri -> {
+                    int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri, userId);
+                    boolean allowed = uriOwnerId == userId;
+                    permissionsOk.set(allowed & permissionsOk.get());
+                });
 
         return permissionsOk.get();
     }
@@ -150,6 +154,47 @@
         return (ok ? rView : null);
     }
 
+    /**
+     * Checks the URI permissions of the icon in the slice, to see if the current userId is able to
+     * access it.
+     *
+     * <p>Returns null if slice contains user inaccessible icons
+     *
+     * <p>TODO: instead of returning a null Slice when the current userId cannot access an icon,
+     * return a reconstructed Slice without the icons. This is currently non-trivial since there are
+     * no public methods to generically add SliceItems to Slices
+     */
+    public static @Nullable Slice sanitizeSlice(Slice slice) {
+        if (slice == null) {
+            return null;
+        }
+
+        int userId = ActivityManager.getCurrentUser();
+
+        // Recontruct the Slice, filtering out bad icons
+        for (SliceItem sliceItem : slice.getItems()) {
+            if (!sliceItem.getFormat().equals(SliceItem.FORMAT_IMAGE)) {
+                // Not an image slice
+                continue;
+            }
+
+            Icon icon = sliceItem.getIcon();
+            if (icon.getType() != Icon.TYPE_URI
+                    && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+                // No URIs to sanitize
+                continue;
+            }
+
+            int iconUriId = android.content.ContentProvider.getUserIdFromUri(icon.getUri(), userId);
+
+            if (iconUriId != userId) {
+                Slog.w(TAG, "sanitizeSlice() user: " + userId + " cannot access icons in Slice");
+                return null;
+            }
+        }
+
+        return slice;
+    }
 
     @Nullable
     static AutofillId[] toArray(@Nullable ArraySet<AutofillId> set) {
diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
index 38a412f..50a26b3 100644
--- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
+++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
@@ -27,6 +27,7 @@
 import android.util.Slog;
 
 import com.android.server.LocalServices;
+import com.android.server.autofill.Helper;
 import com.android.server.autofill.RemoteInlineSuggestionRenderService;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 
@@ -83,6 +84,10 @@
      */
     public boolean renderSuggestion(int width, int height,
             @NonNull IInlineSuggestionUiCallback callback) {
+        if (Helper.sanitizeSlice(mInlinePresentation.getSlice()) == null) {
+            if (sDebug) Slog.d(TAG, "Skipped rendering inline suggestion.");
+            return false;
+        }
         if (mRemoteRenderService != null) {
             if (sDebug) Slog.d(TAG, "Request to recreate the UI");
             mRemoteRenderService.renderSuggestion(callback, mInlinePresentation, width, height,
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index 446b367..c6cb67f 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -553,7 +553,7 @@
             }
             BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,
                     receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,
-                    exported, mService.mPlatformCompat);
+                    exported, callerApp.info, mService.mPlatformCompat);
             if (rl.containsFilter(filter)) {
                 Slog.w(TAG, "Receiver with filter " + filter
                         + " already registered for pid " + rl.pid
diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java
index 3c7fb52..e20c46c 100644
--- a/services/core/java/com/android/server/am/BroadcastFilter.java
+++ b/services/core/java/com/android/server/am/BroadcastFilter.java
@@ -20,6 +20,8 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.os.Binder;
 import android.os.UserHandle;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
@@ -40,7 +42,7 @@
     @ChangeId
     @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
     @VisibleForTesting
-    static final long CHANGE_RESTRICT_PRIORITY_VALUES = 371309185L;
+    static final long RESTRICT_PRIORITY_VALUES = 371309185L;
 
     // Back-pointer to the list this filter is in.
     final ReceiverList receiverList;
@@ -58,7 +60,7 @@
     BroadcastFilter(IntentFilter _filter, ReceiverList _receiverList,
             String _packageName, String _featureId, String _receiverId, String _requiredPermission,
             int _owningUid, int _userId, boolean _instantApp, boolean _visibleToInstantApp,
-            boolean _exported, PlatformCompat platformCompat) {
+            boolean _exported, ApplicationInfo applicationInfo, PlatformCompat platformCompat) {
         super(_filter);
         receiverList = _receiverList;
         packageName = _packageName;
@@ -71,7 +73,8 @@
         visibleToInstantApp = _visibleToInstantApp;
         exported = _exported;
         initialPriority = getPriority();
-        setPriority(calculateAdjustedPriority(owningUid, initialPriority, platformCompat));
+        setPriority(calculateAdjustedPriority(owningUid, initialPriority,
+                applicationInfo, platformCompat));
     }
 
     public @Nullable String getReceiverClassName() {
@@ -125,13 +128,18 @@
 
     @VisibleForTesting
     static int calculateAdjustedPriority(int owningUid, int priority,
-            PlatformCompat platformCompat) {
+            ApplicationInfo applicationInfo, PlatformCompat platformCompat) {
         if (!Flags.restrictPriorityValues()) {
             return priority;
         }
-        if (!platformCompat.isChangeEnabledByUidInternalNoLogging(
-                CHANGE_RESTRICT_PRIORITY_VALUES, owningUid)) {
-            return priority;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            if (!platformCompat.isChangeEnabledInternalNoLogging(
+                    RESTRICT_PRIORITY_VALUES, applicationInfo)) {
+                return priority;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
         if (!UserHandle.isCore(owningUid)) {
             if (priority >= SYSTEM_HIGH_PRIORITY) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 38df10a..e8ce173 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -88,7 +88,7 @@
     @ChangeId
     @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
     @VisibleForTesting
-    static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L;
+    static final long LIMIT_PRIORITY_SCOPE = 371307720L;
 
     final @NonNull Intent intent;    // the original intent that generated us
     final @Nullable ComponentName targetComp; // original component name set on the intent
@@ -781,7 +781,7 @@
         } else {
             if (Flags.limitPriorityScope()) {
                 final boolean[] changeEnabled = calculateChangeStateForReceivers(
-                        receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat);
+                        receivers, LIMIT_PRIORITY_SCOPE, platformCompat);
 
                 // Priority of the previous tranche
                 int lastTranchePriority = 0;
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 82449ce..edad247 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -193,7 +193,6 @@
     private DisplayManagerInternal mDisplayManagerInternal;
 
     private WindowManagerInternal mWindowManagerInternal;
-    private PackageManagerInternal mPackageManagerInternal;
 
     private final File mDoubleTouchGestureEnableFile;
 
@@ -573,7 +572,6 @@
 
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
-        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
 
         mSettingsObserver.registerAndUpdate();
 
@@ -2937,10 +2935,11 @@
     private void enforceManageKeyGesturePermission() {
         // TODO(b/361567988): Use @EnforcePermission to enforce permission once flag guarding the
         //  permission is rolled out
-        if (mSystemReady) {
-            String systemUIPackage = mContext.getString(R.string.config_systemUi);
-            int systemUIAppId = UserHandle.getAppId(mPackageManagerInternal
-                    .getPackageUid(systemUIPackage, PackageManager.MATCH_SYSTEM_ONLY,
+        String systemUIPackage = mContext.getString(R.string.config_systemUi);
+        PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
+        if (pm != null) {
+            int systemUIAppId = UserHandle.getAppId(
+                    pm.getPackageUid(systemUIPackage, PackageManager.MATCH_SYSTEM_ONLY,
                             UserHandle.USER_SYSTEM));
             if (UserHandle.getCallingAppId() == systemUIAppId) {
                 return;
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 f611c57..88a7d08 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -31,6 +31,7 @@
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManagerInternal;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.MessageDeliveryStatus;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
@@ -739,6 +740,14 @@
         return mHubInfoRegistry.getHubs();
     }
 
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public List<HubEndpointInfo> findEndpoints(long endpointId) {
+        super.findEndpoints_enforcePermission();
+        // TODO(b/375487784): connect this with mHubInfoRegistry
+        return Collections.emptyList();
+    }
+
     /**
      * Creates an internal load transaction callback to be used for old API clients
      *
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 436acba..c460465 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -17,7 +17,6 @@
 package com.android.server.media.projection;
 
 import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
-import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
 import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED;
 import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -28,7 +27,6 @@
 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK;
 import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN;
-import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -41,10 +39,7 @@
 import android.app.ActivityOptions.LaunchCookie;
 import android.app.AppOpsManager;
 import android.app.IProcessObserver;
-import android.app.KeyguardManager;
 import android.app.compat.CompatChanges;
-import android.app.role.RoleManager;
-import android.companion.AssociationRequest;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
@@ -74,7 +69,6 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.ContentRecordingSession;
@@ -85,7 +79,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.server.LocalServices;
-import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.Watchdog;
 import com.android.server.wm.WindowManagerInternal;
@@ -140,12 +133,12 @@
     private final ActivityManagerInternal mActivityManagerInternal;
     private final PackageManager mPackageManager;
     private final WindowManagerInternal mWmInternal;
-    private final KeyguardManager mKeyguardManager;
-    private final RoleManager mRoleManager;
+
 
     private final MediaRouter mMediaRouter;
     private final MediaRouterCallback mMediaRouterCallback;
     private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
+    private final MediaProjectionStopController mMediaProjectionStopController;
     private MediaRouter.RouteInfo mMediaRouteInfo;
 
     @GuardedBy("mLock")
@@ -175,72 +168,16 @@
         mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
         mMediaRouterCallback = new MediaRouterCallback();
         mMediaProjectionMetricsLogger = injector.mediaProjectionMetricsLogger(context);
-        mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
-        mKeyguardManager.addKeyguardLockedStateListener(
-                mContext.getMainExecutor(), this::onKeyguardLockedStateChanged);
-        mRoleManager = mContext.getSystemService(RoleManager.class);
+        mMediaProjectionStopController = new MediaProjectionStopController(context,
+                this::maybeStopMediaProjection);
         Watchdog.getInstance().addMonitor(this);
     }
 
-    /**
-     * In order to record the keyguard, the MediaProjection package must be either:
-     *   - a holder of RECORD_SENSITIVE_CONTENT permission, or
-     *   - be one of the bugreport allowlisted packages, or
-     *   - hold the OP_PROJECT_MEDIA AppOp.
-     */
-    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
-    private boolean canCaptureKeyguard() {
-        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
-            return true;
-        }
+    private void maybeStopMediaProjection(int reason) {
         synchronized (mLock) {
-            if (mProjectionGrant == null || mProjectionGrant.packageName == null) {
-                return false;
-            }
-            boolean disableScreenShareProtections = Settings.Global.getInt(
-                    getContext().getContentResolver(),
-                    DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
-            if (disableScreenShareProtections) {
-                Slog.v(TAG,
-                        "Allowing keyguard capture as screenshare protections are disabled.");
-                return true;
-            }
-
-            if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT,
-                    mProjectionGrant.packageName)
-                    == PackageManager.PERMISSION_GRANTED) {
-                Slog.v(TAG,
-                        "Allowing keyguard capture for package with RECORD_SENSITIVE_CONTENT "
-                                + "permission");
-                return true;
-            }
-            if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA,
-                    mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null,
-                    "recording lockscreen")) {
-                // Some tools use media projection by granting the OP_PROJECT_MEDIA app
-                // op via a shell command. Those tools can be granted keyguard capture
-                Slog.v(TAG,
-                        "Allowing keyguard capture for package with OP_PROJECT_MEDIA AppOp ");
-                return true;
-            }
-            if (isProjectionAppHoldingAppStreamingRoleLocked()) {
-                Slog.v(TAG,
-                        "Allowing keyguard capture for package holding app streaming role.");
-                return true;
-            }
-            return SystemConfig.getInstance().getBugreportWhitelistedPackages()
-                    .contains(mProjectionGrant.packageName);
-        }
-    }
-
-    @VisibleForTesting
-    void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
-        if (!isKeyguardLocked) return;
-        synchronized (mLock) {
-            if (mProjectionGrant != null && !canCaptureKeyguard()
-                    && mProjectionGrant.mVirtualDisplayId != INVALID_DISPLAY) {
-                Slog.d(TAG, "Content Recording: Stopped MediaProjection"
-                        + " due to keyguard lock");
+            if (!mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant)) {
+                Slog.d(TAG, "Content Recording: Stopping MediaProjection due to "
+                        + MediaProjectionStopController.stopReasonToString(reason));
                 mProjectionGrant.stop();
             }
         }
@@ -310,6 +247,8 @@
                 }
             });
         }
+
+        mMediaProjectionStopController.startTrackingStopReasons(mContext);
     }
 
     @Override
@@ -736,20 +675,6 @@
         }
     }
 
-    /**
-     * Application holding the app streaming role
-     * ({@value AssociationRequest#DEVICE_PROFILE_APP_STREAMING}) are allowed to record the
-     * lockscreen.
-     *
-     * @return true if the is held by the recording application.
-     */
-    @GuardedBy("mLock")
-    private boolean isProjectionAppHoldingAppStreamingRoleLocked() {
-        return mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
-                        mContext.getUser())
-                .contains(mProjectionGrant.packageName);
-    }
-
     private void dump(final PrintWriter pw) {
         pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
         synchronized (mLock) {
@@ -957,18 +882,19 @@
         public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
             requestConsentForInvalidProjection_enforcePermission();
 
-            if (android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()
-                    && mKeyguardManager.isKeyguardLocked()) {
-                Slog.v(TAG, "Reusing token: Won't request consent while the keyguard is locked");
-                return;
-            }
-
             synchronized (mLock) {
                 if (!isCurrentProjection(projection)) {
                     Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
                             + "isn't current");
                     return;
                 }
+
+                if (mMediaProjectionStopController.isStartForbidden(mProjectionGrant)) {
+                    Slog.v(TAG,
+                            "Reusing token: Won't request consent while MediaProjection is "
+                                    + "restricted");
+                    return;
+                }
             }
 
             // Remove calling app identity before performing any privileged operations.
@@ -1076,7 +1002,6 @@
         }
     }
 
-    @VisibleForTesting
     final class MediaProjection extends IMediaProjection.Stub {
         // Host app has 5 minutes to begin using the token before it is invalid.
         // Some apps show a dialog for the user to interact with (selecting recording resolution)
@@ -1381,12 +1306,15 @@
         @Override
         public void notifyVirtualDisplayCreated(int displayId) {
             notifyVirtualDisplayCreated_enforcePermission();
-            if (mKeyguardManager.isKeyguardLocked() && !canCaptureKeyguard()) {
-                Slog.w(TAG, "Content Recording: Keyguard locked, aborting MediaProjection");
-                stop();
-                return;
-            }
             synchronized (mLock) {
+                if (mMediaProjectionStopController.isStartForbidden(mProjectionGrant)) {
+                    Slog.w(TAG,
+                            "Content Recording: MediaProjection start disallowed, aborting "
+                                    + "MediaProjection");
+                    stop();
+                    return;
+                }
+
                 mVirtualDisplayId = displayId;
 
                 // If prior session was does not have a valid display id, then update the display
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java
new file mode 100644
index 0000000..f5b26c4
--- /dev/null
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.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 com.android.server.media.projection;
+
+import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
+import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
+
+import android.app.AppOpsManager;
+import android.app.KeyguardManager;
+import android.app.role.RoleManager;
+import android.companion.AssociationRequest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.util.Slog;
+import android.view.Display;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemConfig;
+
+import java.util.function.Consumer;
+
+/**
+ * Tracks events that should cause MediaProjection to stop
+ */
+public class MediaProjectionStopController {
+
+    private static final String TAG = "MediaProjectionStopController";
+    @VisibleForTesting
+    static final int STOP_REASON_KEYGUARD = 1;
+    @VisibleForTesting
+    static final int STOP_REASON_CALL_END = 2;
+
+    private final TelephonyCallback mTelephonyCallback = new ProjectionTelephonyCallback();
+    private final Consumer<Integer> mStopReasonConsumer;
+    private final KeyguardManager mKeyguardManager;
+    private final TelecomManager mTelecomManager;
+    private final TelephonyManager mTelephonyManager;
+    private final AppOpsManager mAppOpsManager;
+    private final PackageManager mPackageManager;
+    private final RoleManager mRoleManager;
+    private final ContentResolver mContentResolver;
+
+    private boolean mIsInCall;
+
+    public MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer) {
+        mStopReasonConsumer = stopReasonConsumer;
+        mKeyguardManager = context.getSystemService(KeyguardManager.class);
+        mTelecomManager = context.getSystemService(TelecomManager.class);
+        mTelephonyManager = context.getSystemService(TelephonyManager.class);
+        mAppOpsManager = context.getSystemService(AppOpsManager.class);
+        mPackageManager = context.getPackageManager();
+        mRoleManager = context.getSystemService(RoleManager.class);
+        mContentResolver = context.getContentResolver();
+    }
+
+    /**
+     * Start tracking stop reasons that may interrupt a MediaProjection session.
+     */
+    public void startTrackingStopReasons(Context context) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mKeyguardManager.addKeyguardLockedStateListener(context.getMainExecutor(),
+                    this::onKeyguardLockedStateChanged);
+            if (com.android.media.projection.flags.Flags.stopMediaProjectionOnCallEnd()) {
+                callStateChanged();
+                mTelephonyManager.registerTelephonyCallback(context.getMainExecutor(),
+                        mTelephonyCallback);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Checks whether the given projection grant is exempt from stopping restrictions.
+     */
+    public boolean isExemptFromStopping(
+            MediaProjectionManagerService.MediaProjection projectionGrant) {
+        return isExempt(projectionGrant, false);
+    }
+
+    /**
+     * Apps may disregard recording restrictions via MediaProjection for any stop reason if:
+     * - the "Disable Screenshare protections" developer option is enabled
+     * - the app is a holder of RECORD_SENSITIVE_CONTENT permission
+     * - the app holds the OP_PROJECT_MEDIA AppOp
+     * - the app holds the COMPANION_DEVICE_APP_STREAMING role
+     * - the app is one of the bugreport allowlisted packages
+     * - the current projection does not have an active VirtualDisplay associated with the
+     * MediaProjection session
+     */
+    private boolean isExempt(
+            MediaProjectionManagerService.MediaProjection projectionGrant, boolean forStart) {
+        if (projectionGrant == null || projectionGrant.packageName == null) {
+            return true;
+        }
+        boolean disableScreenShareProtections = Settings.Global.getInt(mContentResolver,
+                DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
+        if (disableScreenShareProtections) {
+            Slog.v(TAG, "Continuing MediaProjection as screenshare protections are disabled.");
+            return true;
+        }
+
+        if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT, projectionGrant.packageName)
+                == PackageManager.PERMISSION_GRANTED) {
+            Slog.v(TAG,
+                    "Continuing MediaProjection for package with RECORD_SENSITIVE_CONTENT "
+                            + "permission");
+            return true;
+        }
+        if (AppOpsManager.MODE_ALLOWED == mAppOpsManager.noteOpNoThrow(
+                AppOpsManager.OP_PROJECT_MEDIA, projectionGrant.uid,
+                projectionGrant.packageName, /* attributionTag= */ null, "recording lockscreen")) {
+            // Some tools use media projection by granting the OP_PROJECT_MEDIA app
+            // op via a shell command.
+            Slog.v(TAG, "Continuing MediaProjection for package with OP_PROJECT_MEDIA AppOp ");
+            return true;
+        }
+        if (mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+                projectionGrant.userHandle).contains(projectionGrant.packageName)) {
+            Slog.v(TAG, "Continuing MediaProjection for package holding app streaming role.");
+            return true;
+        }
+        if (SystemConfig.getInstance().getBugreportWhitelistedPackages().contains(
+                projectionGrant.packageName)) {
+            Slog.v(TAG, "Continuing MediaProjection for package allowlisted for bugreporting.");
+            return true;
+        }
+        if (!forStart && projectionGrant.getVirtualDisplayId() == Display.INVALID_DISPLAY) {
+            Slog.v(TAG, "Continuing MediaProjection as current projection has no VirtualDisplay.");
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return {@code true} if a MediaProjection session is currently in a restricted state.
+     */
+    public boolean isStartForbidden(
+            MediaProjectionManagerService.MediaProjection projectionGrant) {
+        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
+            return false;
+        }
+
+        if (!mKeyguardManager.isKeyguardLocked()) {
+            return false;
+        }
+
+        if (isExempt(projectionGrant, true)) {
+            return false;
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
+        if (!isKeyguardLocked) return;
+        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
+            return;
+        }
+        mStopReasonConsumer.accept(STOP_REASON_KEYGUARD);
+    }
+
+    @VisibleForTesting
+    void callStateChanged() {
+        if (!com.android.media.projection.flags.Flags.stopMediaProjectionOnCallEnd()) {
+            return;
+        }
+        boolean isInCall = mTelecomManager.isInCall();
+        if (isInCall == mIsInCall) {
+            return;
+        }
+        if (mIsInCall && !isInCall) {
+            mStopReasonConsumer.accept(STOP_REASON_CALL_END);
+        }
+        mIsInCall = isInCall;
+    }
+
+    /**
+     * @return a String representation of the stop reason interrupting MediaProjection.
+     */
+    public static String stopReasonToString(int stopReason) {
+        switch (stopReason) {
+            case STOP_REASON_KEYGUARD -> {
+                return "STOP_REASON_KEYGUARD";
+            }
+            case STOP_REASON_CALL_END -> {
+                return "STOP_REASON_CALL_END";
+            }
+        }
+        return "";
+    }
+
+    private final class ProjectionTelephonyCallback extends TelephonyCallback implements
+            TelephonyCallback.CallStateListener {
+        @Override
+        public void onCallStateChanged(int state) {
+            callStateChanged();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 5914dbe..7fd9620 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -92,10 +92,16 @@
     static final int REGROUP_REASON_CHANNEL_UPDATE = 0;
     // Regrouping needed because of notification bundling
     static final int REGROUP_REASON_BUNDLE = 1;
+    // Regrouping needed because of notification unbundling
+    static final int REGROUP_REASON_UNBUNDLE = 2;
+    // Regrouping needed because of notification unbundling + the original group summary exists
+    static final int REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP = 3;
 
     @IntDef(prefix = { "REGROUP_REASON_" }, value = {
         REGROUP_REASON_CHANNEL_UPDATE,
         REGROUP_REASON_BUNDLE,
+        REGROUP_REASON_UNBUNDLE,
+        REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface RegroupingReason {}
@@ -103,7 +109,6 @@
     private final Callback mCallback;
     private final int mAutoGroupAtCount;
     private final int mAutogroupSparseGroupsAtCount;
-    private final int mAutoGroupRegroupingAtCount;
     private final Context mContext;
     private final PackageManager mPackageManager;
     private boolean mIsTestHarnessExempted;
@@ -190,11 +195,6 @@
         mContext = context;
         mPackageManager = packageManager;
         mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount;
-        if (notificationRegroupOnClassification()) {
-            mAutoGroupRegroupingAtCount = 1;
-        } else {
-            mAutoGroupRegroupingAtCount = mAutoGroupAtCount;
-        }
         NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections();
     }
 
@@ -797,7 +797,8 @@
                         Slog.v(TAG, "isGroupChildInDifferentBundleThanSummary: " + record);
                     }
                     moveNotificationsToNewSection(record.getUserId(), pkgName,
-                            List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)));
+                            List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)),
+                            REGROUP_REASON_BUNDLE);
                     return;
                 }
             }
@@ -945,6 +946,27 @@
         }
     }
 
+    /**
+     * Called when a notification that was classified (bundled) is restored to its original channel.
+     * The notification will be restored to its original group, if any/if summary still exists.
+     * Otherwise it will be moved to the appropriate section as an ungrouped notification.
+     *
+     * @param record the notification which had its channel updated
+     * @param originalSummaryExists the original group summary exists
+     */
+    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void onNotificationUnbundled(final NotificationRecord record,
+            final boolean originalSummaryExists) {
+        synchronized (mAggregatedNotifications) {
+            ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
+            notificationsToCheck.put(record.getKey(), record);
+            regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
+                    notificationsToCheck,
+                    originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP
+                        : REGROUP_REASON_UNBUNDLE);
+        }
+    }
+
     @GuardedBy("mAggregatedNotifications")
     private void regroupNotifications(int userId, String pkgName,
             ArrayMap<String, NotificationRecord> notificationsToCheck,
@@ -973,7 +995,7 @@
 
         // Batch move to new section
         if (!notificationsToMove.isEmpty()) {
-            moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
+            moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupingReason);
         }
     }
 
@@ -1093,7 +1115,7 @@
 
     @GuardedBy("mAggregatedNotifications")
     private void moveNotificationsToNewSection(final int userId, final String pkgName,
-            final List<NotificationMoveOp> notificationsToMove) {
+            final List<NotificationMoveOp> notificationsToMove, int regroupingReason) {
         record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record,
                              boolean hasSummary) { }
         // Bundled operations to apply to groups affected by the channel update
@@ -1111,7 +1133,8 @@
             if (DEBUG) {
                 Log.i(TAG,
                     "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey
-                        + " from: " + oldFullAggregateGroupKey);
+                            + " from: " + oldFullAggregateGroupKey + " regroupingReason: "
+                            + regroupingReason);
             }
 
             // Update/remove aggregate summary for old group
@@ -1140,28 +1163,35 @@
             // Add moved notifications to the ungrouped list for new group and do grouping
             // after all notifications have been handled
             if (newFullAggregateGroupKey != null) {
-                final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs =
+                if (notificationRegroupOnClassification()
+                        && regroupingReason == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) {
+                    // Just reset override group key, original summary exists
+                    // => will be grouped back to its original group
+                    record.setOverrideGroupKey(null);
+                } else {
+                    final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs =
                         mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey,
                             new ArrayMap<>());
-                boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty();
-                ArrayMap<String, NotificationAttributes> ungrouped =
+                    boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty();
+                    ArrayMap<String, NotificationAttributes> ungrouped =
                         mUngroupedAbuseNotifications.getOrDefault(newFullAggregateGroupKey,
                             new ArrayMap<>());
-                ungrouped.put(record.getKey(), new NotificationAttributes(
+                    ungrouped.put(record.getKey(), new NotificationAttributes(
                         record.getFlags(),
                         record.getNotification().getSmallIcon(),
                         record.getNotification().color,
                         record.getNotification().visibility,
                         record.getNotification().getGroupAlertBehavior(),
                         record.getChannel().getId()));
-                mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped);
+                    mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped);
 
-                record.setOverrideGroupKey(null);
+                    record.setOverrideGroupKey(null);
 
-                // Only add once, for triggering notification
-                if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) {
-                    groupsToUpdate.put(newFullAggregateGroupKey,
-                        new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary));
+                    // Only add once, for triggering notification
+                    if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) {
+                        groupsToUpdate.put(newFullAggregateGroupKey,
+                            new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary));
+                    }
                 }
             }
         }
@@ -1176,7 +1206,7 @@
             NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record;
             boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary;
             //Group needs to be created/updated
-            if (ungrouped.size() >= mAutoGroupRegroupingAtCount
+            if (ungrouped.size() >= mAutoGroupAtCount
                     || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) {
                 NotificationSectioner sectioner = getSection(triggeringNotification);
                 if (sectioner == null) {
@@ -1436,7 +1466,8 @@
         }
     }
 
-    private ArrayMap<String, NotificationRecord> getSparseGroups(
+    @VisibleForTesting
+    protected ArrayMap<String, NotificationRecord> getSparseGroups(
             final FullyQualifiedGroupKey fullAggregateGroupKey,
             final List<NotificationRecord> notificationList,
             final Map<String, NotificationRecord> summaryByGroupKey,
@@ -1448,8 +1479,8 @@
                         && summary.getUserId() == fullAggregateGroupKey.userId
                         && summary.getSbn().isAppGroup()
                         && !summary.getGroupKey().equals(fullAggregateGroupKey.toString())) {
-                    int numChildren = getNumChildrenForGroup(summary.getSbn().getGroup(),
-                            notificationList);
+                    int numChildren = getNumChildrenForGroupWithSection(summary.getSbn().getGroup(),
+                            notificationList, sectioner);
                     if (numChildren > 0 && numChildren < MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING) {
                         sparseGroups.put(summary.getGroupKey(), summary);
                     }
@@ -1459,6 +1490,43 @@
         return sparseGroups;
     }
 
+    /**
+     *  Get the number of children of a group if all match a certain section.
+     *  Used for force grouping sparse groups, where the summary may match a section but the
+     *  child notifications do not: ie. conversations
+     *
+     * @param groupKey the group key (name)
+     * @param notificationList all notifications list
+     * @param sectioner the section to match
+     * @return number of children in that group or -1 if section does not match
+     */
+    private int getNumChildrenForGroupWithSection(final String groupKey,
+            final List<NotificationRecord> notificationList,
+            final NotificationSectioner sectioner) {
+        int numChildren = 0;
+        for (NotificationRecord r : notificationList) {
+            if (!r.getNotification().isGroupSummary() && groupKey.equals(r.getSbn().getGroup())) {
+                NotificationSectioner childSection = getSection(r);
+                if (childSection == null || childSection != sectioner) {
+                    if (DEBUG) {
+                        Slog.i(TAG,
+                                "getNumChildrenForGroupWithSection skip because invalid section: "
+                                    + groupKey + " r: " + r);
+                    }
+                    return -1;
+                } else {
+                    numChildren++;
+                }
+            }
+        }
+
+        if (DEBUG) {
+            Slog.i(TAG,
+                    "getNumChildrenForGroupWithSection " + groupKey + " numChild: " + numChildren);
+        }
+        return numChildren;
+    }
+
     @GuardedBy("mAggregatedNotifications")
     private void cacheCanceledSummary(NotificationRecord record) {
         final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(),
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0d8880a..5182dfe 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -7180,13 +7180,16 @@
                         Slog.i(TAG, "Removing app summary (all children bundled): "
                                 + groupSummary);
                     }
-                    canceledSummary = groupSummary;
-                    mSummaryByGroupKey.remove(oldGroupKey);
-                    cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(),
+                    if (convertSummaryToNotificationLocked(groupSummary.getKey())) {
+                        groupSummary.isCanceled = true;
+                        canceledSummary = groupSummary;
+                        mSummaryByGroupKey.remove(oldGroupKey);
+                        cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(),
                             groupSummary.getSbn().getPackageName(),
                             groupSummary.getSbn().getTag(),
                             groupSummary.getSbn().getId(), 0, 0, false, groupSummary.getUserId(),
                             NotificationListenerService.REASON_GROUP_OPTIMIZATION, null);
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 06e29c2..b2b8aaf 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -4984,7 +4984,10 @@
             res.getValue(com.android.internal.R.string.owner_name, mOwnerNameTypedValue, true);
             final CharSequence ownerName = mOwnerNameTypedValue.coerceToString();
             mOwnerName.set(ownerName != null ? ownerName.toString() : null);
+            // Invalidate when owners name changes due to config change.
+            UserManager.invalidateCacheOnUserDataChanged();
         }
+
     }
 
     private void scheduleWriteUserList() {
@@ -4997,6 +5000,8 @@
             Message msg = mHandler.obtainMessage(WRITE_USER_LIST_MSG);
             mHandler.sendMessageDelayed(msg, WRITE_USER_DELAY);
         }
+        // Invalidate cache when {@link UserData} changed, but write was scheduled for later.
+        UserManager.invalidateCacheOnUserDataChanged();
     }
 
     private void scheduleWriteUser(@UserIdInt int userId) {
@@ -5009,6 +5014,8 @@
             Message msg = mHandler.obtainMessage(WRITE_USER_MSG, userId);
             mHandler.sendMessageDelayed(msg, WRITE_USER_DELAY);
         }
+        // Invalidate cache when {@link Data} changed, but write was scheduled for later.
+        UserManager.invalidateCacheOnUserDataChanged();
     }
 
     private ResilientAtomicFile getUserFile(int userId) {
@@ -5032,6 +5039,9 @@
         if (DBG) {
             debug("writeUserLP " + userData);
         }
+        // invalidate caches related to any {@link UserData} change.
+        UserManager.invalidateCacheOnUserDataChanged();
+
         try (ResilientAtomicFile userFile = getUserFile(userData.info.id)) {
             FileOutputStream fos = null;
             try {
@@ -5196,6 +5206,8 @@
         if (DBG) {
             debug("writeUserList");
         }
+        // invalidate caches related to any {@link UserData} change.
+        UserManager.invalidateCacheOnUserDataChanged();
 
         try (ResilientAtomicFile file = getUserListFile()) {
             FileOutputStream fos = null;
@@ -7958,7 +7970,7 @@
                                     Settings.Secure.getIntForUser(mContext.getContentResolver(),
                                             HIDE_PRIVATESPACE_ENTRY_POINT, parentId) == 1);
                         } catch (Settings.SettingNotFoundException e) {
-                            throw new RuntimeException(e);
+                            config.putBoolean(PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false);
                         }
                     }
                     return new LauncherUserInfo.Builder(userDetails.getName(),
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 0acfe92..37883f5 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2332,6 +2332,8 @@
         Trace.traceBegin(Trace.TRACE_TAG_POWER, traceMethodName);
         try {
             // Phase 2: Handle wakefulness change and bookkeeping.
+            // Under lock, invalidate before set ensures caches won't return stale values.
+            mInjector.invalidateIsInteractiveCaches();
             mWakefulnessRaw = newWakefulness;
             mWakefulnessChanging = true;
             mDirty |= DIRTY_WAKEFULNESS;
@@ -2429,7 +2431,6 @@
     void onPowerGroupEventLocked(int event, PowerGroup powerGroup) {
         mWakefulnessChanging = true;
         mDirty |= DIRTY_WAKEFULNESS;
-        mInjector.invalidateIsInteractiveCaches();
         final int groupId = powerGroup.getGroupId();
         if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_REMOVED) {
             mPowerGroups.delete(groupId);
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 2c0ce25..17459df 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -33,11 +33,15 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.hardware.power.ChannelConfig;
+import android.hardware.power.CpuHeadroomParams;
+import android.hardware.power.GpuHeadroomParams;
 import android.hardware.power.IPower;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
 import android.os.Binder;
+import android.os.CpuHeadroomParamsInternal;
+import android.os.GpuHeadroomParamsInternal;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IHintManager;
@@ -90,6 +94,10 @@
 
     private static final int EVENT_CLEAN_UP_UID = 3;
     @VisibleForTesting  static final int CLEAN_UP_UID_DELAY_MILLIS = 1000;
+    private static final int DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS = 1000;
+    private static final int DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS = 1000;
+    private static final int HEADROOM_INTERVAL_UNSUPPORTED = -1;
+    @VisibleForTesting static final int DEFAULT_HEADROOM_PID = -1;
 
 
     @VisibleForTesting final long mHintSessionPreferredRate;
@@ -160,10 +168,76 @@
 
     private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint";
     private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";
+    private static final String PROPERTY_USE_HAL_HEADROOMS = "persist.hms.use_hal_headrooms";
 
     private Boolean mFMQUsesIntegratedEventFlag = false;
 
-    @VisibleForTesting final IHintManager.Stub mService = new BinderService();
+    private final Object mCpuHeadroomLock = new Object();
+
+    private static class CpuHeadroomCacheItem {
+        long mExpiredTimeMillis;
+        CpuHeadroomParamsInternal mParams;
+        float[] mHeadroom;
+        long mPid;
+
+        CpuHeadroomCacheItem(long expiredTimeMillis, CpuHeadroomParamsInternal params,
+                float[] headroom, long pid) {
+            mExpiredTimeMillis = expiredTimeMillis;
+            mParams = params;
+            mPid = pid;
+            mHeadroom = headroom;
+        }
+
+        private boolean match(CpuHeadroomParamsInternal params, long pid) {
+            if (mParams == null && params == null) return true;
+            if (mParams != null) {
+                return mParams.equals(params) && pid == mPid;
+            }
+            return false;
+        }
+
+        private boolean isExpired() {
+            return System.currentTimeMillis() > mExpiredTimeMillis;
+        }
+    }
+
+    @GuardedBy("mCpuHeadroomLock")
+    private final List<CpuHeadroomCacheItem> mCpuHeadroomCache;
+    private final long mCpuHeadroomIntervalMillis;
+
+    private final Object mGpuHeadroomLock = new Object();
+
+    private static class GpuHeadroomCacheItem {
+        long mExpiredTimeMillis;
+        GpuHeadroomParamsInternal mParams;
+        float mHeadroom;
+
+        GpuHeadroomCacheItem(long expiredTimeMillis, GpuHeadroomParamsInternal params,
+                float headroom) {
+            mExpiredTimeMillis = expiredTimeMillis;
+            mParams = params;
+            mHeadroom = headroom;
+        }
+
+        private boolean match(GpuHeadroomParamsInternal params) {
+            if (mParams == null && params == null) return true;
+            if (mParams != null) {
+                return mParams.equals(params);
+            }
+            return false;
+        }
+
+        private boolean isExpired() {
+            return System.currentTimeMillis() > mExpiredTimeMillis;
+        }
+    }
+
+    @GuardedBy("mGpuHeadroomLock")
+    private final List<GpuHeadroomCacheItem> mGpuHeadroomCache;
+    private final long mGpuHeadroomIntervalMillis;
+
+    @VisibleForTesting
+    final IHintManager.Stub mService = new BinderService();
 
     public HintManagerService(Context context) {
         this(context, new Injector());
@@ -197,13 +271,72 @@
         mPowerHal = injector.createIPower();
         mPowerHalVersion = 0;
         mUsesFmq = false;
+        long cpuHeadroomIntervalMillis = HEADROOM_INTERVAL_UNSUPPORTED;
+        long gpuHeadroomIntervalMillis = HEADROOM_INTERVAL_UNSUPPORTED;
         if (mPowerHal != null) {
             try {
                 mPowerHalVersion = mPowerHal.getInterfaceVersion();
+                if (mPowerHal.getInterfaceVersion() >= 6) {
+                    if (SystemProperties.getBoolean(PROPERTY_USE_HAL_HEADROOMS, true)) {
+                        cpuHeadroomIntervalMillis = checkCpuHeadroomSupport();
+                        gpuHeadroomIntervalMillis = checkGpuHeadroomSupport();
+                    }
+                }
             } catch (RemoteException e) {
                 throw new IllegalStateException("Could not contact PowerHAL!", e);
             }
         }
+        mCpuHeadroomIntervalMillis = cpuHeadroomIntervalMillis;
+        mGpuHeadroomIntervalMillis = gpuHeadroomIntervalMillis;
+        if (mCpuHeadroomIntervalMillis > 0) {
+            mCpuHeadroomCache = new ArrayList<>(4);
+        } else {
+            mCpuHeadroomCache = null;
+        }
+        if (mGpuHeadroomIntervalMillis > 0) {
+            mGpuHeadroomCache = new ArrayList<>(2);
+        } else {
+            mGpuHeadroomCache = null;
+        }
+    }
+
+    private long checkCpuHeadroomSupport() {
+        try {
+            synchronized (mCpuHeadroomLock) {
+                final CpuHeadroomParams defaultParams = new CpuHeadroomParams();
+                defaultParams.pid = Process.myPid();
+                float[] ret = mPowerHal.getCpuHeadroom(defaultParams);
+                if (ret != null && ret.length > 0) {
+                    return Math.max(
+                            DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS,
+                            mPowerHal.getCpuHeadroomMinIntervalMillis());
+                }
+            }
+
+        } catch (UnsupportedOperationException e) {
+            Slog.w(TAG, "getCpuHeadroom HAL API is not supported", e);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "getCpuHeadroom HAL API fails, disabling the API", e);
+        }
+        return HEADROOM_INTERVAL_UNSUPPORTED;
+    }
+
+    private long checkGpuHeadroomSupport() {
+        try {
+            synchronized (mGpuHeadroomLock) {
+                float ret = mPowerHal.getGpuHeadroom(new GpuHeadroomParams());
+                if (!Float.isNaN(ret)) {
+                    return Math.max(
+                            DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS,
+                            mPowerHal.getGpuHeadroomMinIntervalMillis());
+                }
+            }
+        } catch (UnsupportedOperationException e) {
+            Slog.w(TAG, "getGpuHeadroom HAL API is not supported", e);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "getGpuHeadroom HAL API fails, disabling the API", e);
+        }
+        return HEADROOM_INTERVAL_UNSUPPORTED;
     }
 
     private ServiceThread createCleanUpThread() {
@@ -738,7 +871,7 @@
                 mLinked = false;
             }
             if (mConfig != null) {
-                try  {
+                try {
                     mPowerHal.closeSessionChannel(mTgid, mUid);
                 } catch (RemoteException e) {
                     throw new IllegalStateException("Failed to close session channel!", e);
@@ -982,13 +1115,13 @@
     }
 
     // returns the first invalid tid or null if not found
-    private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
+    private Integer checkTidValid(int uid, int tgid, int[] tids, IntArray nonIsolated) {
         // Make sure all tids belongs to the same UID (including isolated UID),
         // tids can belong to different application processes.
         List<Integer> isolatedPids = null;
         for (int i = 0; i < tids.length; i++) {
             int tid = tids[i];
-            final String[] procStatusKeys = new String[] {
+            final String[] procStatusKeys = new String[]{
                     "Uid:",
                     "Tgid:"
             };
@@ -1058,7 +1191,7 @@
                     Slogf.w(TAG, errMsg);
                     throw new SecurityException(errMsg);
                 }
-                if (resetOnForkEnabled()){
+                if (resetOnForkEnabled()) {
                     try {
                         for (int tid : tids) {
                             int policy = Process.getThreadScheduler(tid);
@@ -1214,6 +1347,124 @@
         }
 
         @Override
+        public float[] getCpuHeadroom(@Nullable CpuHeadroomParamsInternal params) {
+            if (mCpuHeadroomIntervalMillis <= 0) {
+                throw new UnsupportedOperationException();
+            }
+            CpuHeadroomParams halParams = new CpuHeadroomParams();
+            halParams.pid = Binder.getCallingPid();
+            if (params != null) {
+                halParams.calculationType = params.calculationType;
+                halParams.selectionType = params.selectionType;
+                if (params.usesDeviceHeadroom) {
+                    halParams.pid = DEFAULT_HEADROOM_PID;
+                }
+            }
+            synchronized (mCpuHeadroomLock) {
+                while (!mCpuHeadroomCache.isEmpty()) {
+                    if (mCpuHeadroomCache.getFirst().isExpired()) {
+                        mCpuHeadroomCache.removeFirst();
+                    } else {
+                        break;
+                    }
+                }
+                for (int i = 0; i < mCpuHeadroomCache.size(); ++i) {
+                    final CpuHeadroomCacheItem item = mCpuHeadroomCache.get(i);
+                    if (item.match(params, halParams.pid)) {
+                        item.mExpiredTimeMillis =
+                                System.currentTimeMillis() + mCpuHeadroomIntervalMillis;
+                        mCpuHeadroomCache.remove(i);
+                        mCpuHeadroomCache.add(item);
+                        return item.mHeadroom;
+                    }
+                }
+            }
+            // return from HAL directly
+            try {
+                float[] headroom = mPowerHal.getCpuHeadroom(halParams);
+                if (headroom == null || headroom.length == 0) {
+                    Slog.wtf(TAG,
+                            "CPU headroom from Power HAL is invalid: " + Arrays.toString(headroom));
+                    return new float[]{Float.NaN};
+                }
+                synchronized (mCpuHeadroomLock) {
+                    mCpuHeadroomCache.add(new CpuHeadroomCacheItem(
+                            System.currentTimeMillis() + mCpuHeadroomIntervalMillis,
+                            params, headroom, halParams.pid
+                    ));
+                }
+                return headroom;
+
+            } catch (RemoteException e) {
+                return new float[]{Float.NaN};
+            }
+        }
+
+        @Override
+        public float getGpuHeadroom(@Nullable GpuHeadroomParamsInternal params) {
+            if (mGpuHeadroomIntervalMillis <= 0) {
+                throw new UnsupportedOperationException();
+            }
+            GpuHeadroomParams halParams = new GpuHeadroomParams();
+            if (params != null) {
+                halParams.calculationType = params.calculationType;
+            }
+            synchronized (mGpuHeadroomLock) {
+                while (!mGpuHeadroomCache.isEmpty()) {
+                    if (mGpuHeadroomCache.getFirst().isExpired()) {
+                        mGpuHeadroomCache.removeFirst();
+                    } else {
+                        break;
+                    }
+                }
+                for (int i = 0; i < mGpuHeadroomCache.size(); ++i) {
+                    final GpuHeadroomCacheItem item = mGpuHeadroomCache.get(i);
+                    if (item.match(params)) {
+                        item.mExpiredTimeMillis =
+                                System.currentTimeMillis() + mGpuHeadroomIntervalMillis;
+                        mGpuHeadroomCache.remove(i);
+                        mGpuHeadroomCache.add(item);
+                        return item.mHeadroom;
+                    }
+                }
+            }
+            // return from HAL directly
+            try {
+                float headroom = mPowerHal.getGpuHeadroom(halParams);
+                if (Float.isNaN(headroom)) {
+                    Slog.wtf(TAG,
+                            "GPU headroom from Power HAL is NaN");
+                    return Float.NaN;
+                }
+                synchronized (mGpuHeadroomLock) {
+                    mGpuHeadroomCache.add(new GpuHeadroomCacheItem(
+                            System.currentTimeMillis() + mGpuHeadroomIntervalMillis,
+                            params, headroom
+                    ));
+                }
+                return headroom;
+            } catch (RemoteException e) {
+                return Float.NaN;
+            }
+        }
+
+        @Override
+        public long getCpuHeadroomMinIntervalMillis() throws RemoteException {
+            if (mCpuHeadroomIntervalMillis <= 0) {
+                throw new UnsupportedOperationException();
+            }
+            return mCpuHeadroomIntervalMillis;
+        }
+
+        @Override
+        public long getGpuHeadroomMinIntervalMillis() throws RemoteException {
+            if (mGpuHeadroomIntervalMillis <= 0) {
+                throw new UnsupportedOperationException();
+            }
+            return mGpuHeadroomIntervalMillis;
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
                 return;
@@ -1235,6 +1486,25 @@
                     }
                 }
             }
+            pw.println("CPU Headroom Interval: " + mCpuHeadroomIntervalMillis);
+            pw.println("GPU Headroom Interval: " + mGpuHeadroomIntervalMillis);
+            try {
+                CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
+                params.selectionType = CpuHeadroomParams.SelectionType.ALL;
+                params.usesDeviceHeadroom = true;
+                pw.println("CPU headroom: " + Arrays.toString(getCpuHeadroom(params)));
+                params = new CpuHeadroomParamsInternal();
+                params.selectionType = CpuHeadroomParams.SelectionType.PER_CORE;
+                params.usesDeviceHeadroom = true;
+                pw.println("CPU headroom per core: " + Arrays.toString(getCpuHeadroom(params)));
+            } catch (Exception e) {
+                pw.println("CPU headroom: N/A");
+            }
+            try {
+                pw.println("GPU headroom: " + getGpuHeadroom(null));
+            } catch (Exception e) {
+                pw.println("GPU headroom: N/A");
+            }
         }
 
         private void logPerformanceHintSessionAtom(int uid, long sessionId,
@@ -1467,7 +1737,7 @@
                             Slogf.w(TAG, errMsg);
                             throw new SecurityException(errMsg);
                         }
-                        if (resetOnForkEnabled()){
+                        if (resetOnForkEnabled()) {
                             try {
                                 for (int tid : tids) {
                                     int policy = Process.getThreadScheduler(tid);
diff --git a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
similarity index 69%
rename from services/core/java/com/android/server/security/forensic/BackupTransportConnection.java
rename to services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
index caca011..b85199e 100644
--- a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java
+++ b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
@@ -16,15 +16,19 @@
 
 package com.android.server.security.forensic;
 
+import static android.Manifest.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.security.forensic.ForensicEvent;
-import android.security.forensic.IBackupTransport;
+import android.security.forensic.IForensicEventTransport;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -36,20 +40,20 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-public class BackupTransportConnection implements ServiceConnection {
-    private static final String TAG = "BackupTransportConnection";
+public class ForensicEventTransportConnection implements ServiceConnection {
+    private static final String TAG = "ForensicEventTransportConnection";
     private static final long FUTURE_TIMEOUT_MILLIS = 60 * 1000; // 1 mins
     private final Context mContext;
-    private String mForensicBackupTransportConfig;
-    volatile IBackupTransport mService;
+    private String mForensicEventTransportConfig;
+    volatile IForensicEventTransport mService;
 
-    public BackupTransportConnection(Context context) {
+    public ForensicEventTransportConnection(Context context) {
         mContext = context;
         mService = null;
     }
 
     /**
-     * Initialize the BackupTransport binder service.
+     * Initialize the ForensicEventTransport binder service.
      * @return Whether the initialization succeed.
      */
     public boolean initialize() {
@@ -74,7 +78,7 @@
     }
 
     /**
-     * Add data to the BackupTransport binder service.
+     * Add data to the ForensicEventTransport binder service.
      * @param data List of ForensicEvent.
      * @return Whether the data is added to the binder service.
      */
@@ -109,21 +113,37 @@
             return future.get(FUTURE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException
                  | CancellationException e) {
-            Slog.w(TAG, "Failed to get result from transport:", e);
+            Slog.e(TAG, "Failed to get result from transport:", e);
             return null;
         }
     }
 
     private boolean bindService() {
-        mForensicBackupTransportConfig = mContext.getString(
-                com.android.internal.R.string.config_forensicBackupTransport);
-        if (TextUtils.isEmpty(mForensicBackupTransportConfig)) {
+        mForensicEventTransportConfig = mContext.getString(
+                com.android.internal.R.string.config_forensicEventTransport);
+        if (TextUtils.isEmpty(mForensicEventTransportConfig)) {
+            Slog.e(TAG, "config_forensicEventTransport is empty");
             return false;
         }
 
         ComponentName serviceComponent =
-                ComponentName.unflattenFromString(mForensicBackupTransportConfig);
+                ComponentName.unflattenFromString(mForensicEventTransportConfig);
         if (serviceComponent == null) {
+            Slog.e(TAG, "Can't get serviceComponent name");
+            return false;
+        }
+
+        try {
+            ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(serviceComponent,
+                    0 /* flags */);
+            if (!BIND_FORENSIC_EVENT_TRANSPORT_SERVICE.equals(serviceInfo.permission)) {
+                Slog.e(TAG, serviceComponent.flattenToShortString()
+                        + " is not declared with the permission "
+                        + "\"" + BIND_FORENSIC_EVENT_TRANSPORT_SERVICE + "\"");
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Unable to find serviceComponent");
             return false;
         }
 
@@ -143,7 +163,7 @@
 
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        mService = IBackupTransport.Stub.asInterface(service);
+        mService = IForensicEventTransport.Stub.asInterface(service);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java
index 01f630b..2be068f 100644
--- a/services/core/java/com/android/server/security/forensic/ForensicService.java
+++ b/services/core/java/com/android/server/security/forensic/ForensicService.java
@@ -16,11 +16,16 @@
 
 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;
@@ -41,16 +46,15 @@
 public class ForensicService extends SystemService {
     private static final String TAG = "ForensicService";
 
-    private static final int MSG_MONITOR_STATE = 0;
-    private static final int MSG_MAKE_VISIBLE = 1;
-    private static final int MSG_MAKE_INVISIBLE = 2;
-    private static final int MSG_ENABLE = 3;
-    private static final int MSG_DISABLE = 4;
-    private static final int MSG_BACKUP = 5;
+    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_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
-    private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+    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;
@@ -58,19 +62,19 @@
             IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
     private static final int ERROR_INVALID_STATE_TRANSITION =
             IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
-    private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+    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 BackupTransportConnection mBackupTransportConnection;
+    private final ForensicEventTransportConnection mForensicEventTransportConnection;
     private final DataAggregator mDataAggregator;
     private final BinderService mBinderService;
 
-    private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>();
-    private volatile int mState = STATE_INVISIBLE;
+    private final ArrayList<IForensicServiceStateCallback> mStateCallbacks = new ArrayList<>();
+    private volatile int mState = STATE_DISABLED;
 
     public ForensicService(@NonNull Context context) {
         this(new InjectorImpl(context));
@@ -81,9 +85,9 @@
         super(injector.getContext());
         mContext = injector.getContext();
         mHandler = new EventHandler(injector.getLooper(), this);
-        mBackupTransportConnection = injector.getBackupTransportConnection();
+        mForensicEventTransportConnection = injector.getForensicEventransportConnection();
         mDataAggregator = injector.getDataAggregator(this);
-        mBinderService = new BinderService(this);
+        mBinderService = new BinderService(this, injector.getPermissionEnforcer());
     }
 
     @VisibleForTesting
@@ -94,32 +98,36 @@
     private static final class BinderService extends IForensicService.Stub {
         final ForensicService mService;
 
-        BinderService(ForensicService service)  {
+        BinderService(ForensicService service, @NonNull PermissionEnforcer permissionEnforcer)  {
+            super(permissionEnforcer);
             mService = service;
         }
 
         @Override
-        public void monitorState(IForensicServiceStateCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MONITOR_STATE, callback).sendToTarget();
+        @EnforcePermission(READ_FORENSIC_STATE)
+        public void addStateCallback(IForensicServiceStateCallback callback) {
+            addStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_ADD_STATE_CALLBACK, callback).sendToTarget();
         }
 
         @Override
-        public void makeVisible(IForensicServiceCommandCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MAKE_VISIBLE, callback).sendToTarget();
+        @EnforcePermission(READ_FORENSIC_STATE)
+        public void removeStateCallback(IForensicServiceStateCallback callback) {
+            removeStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_REMOVE_STATE_CALLBACK, callback).sendToTarget();
         }
 
         @Override
-        public void makeInvisible(IForensicServiceCommandCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MAKE_INVISIBLE, 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();
         }
     }
@@ -135,24 +143,18 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_MONITOR_STATE:
+                case MSG_ADD_STATE_CALLBACK:
                     try {
-                        mService.monitorState(
+                        mService.addStateCallback(
                                 (IForensicServiceStateCallback) msg.obj);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "RemoteException", e);
                     }
                     break;
-                case MSG_MAKE_VISIBLE:
+                case MSG_REMOVE_STATE_CALLBACK:
                     try {
-                        mService.makeVisible((IForensicServiceCommandCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_MAKE_INVISIBLE:
-                    try {
-                        mService.makeInvisible((IForensicServiceCommandCallback) msg.obj);
+                        mService.removeStateCallback(
+                                (IForensicServiceStateCallback) msg.obj);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "RemoteException", e);
                     }
@@ -171,8 +173,8 @@
                         Slog.e(TAG, "RemoteException", e);
                     }
                     break;
-                case MSG_BACKUP:
-                    mService.backup((List<ForensicEvent>) msg.obj);
+                case MSG_TRANSPORT:
+                    mService.transport((List<ForensicEvent>) msg.obj);
                     break;
                 default:
                     Slog.w(TAG, "Unknown message: " + msg.what);
@@ -180,103 +182,83 @@
         }
     }
 
-    private void monitorState(IForensicServiceStateCallback callback) throws RemoteException {
-        for (int i = 0; i < mStateMonitors.size(); i++) {
-            if (mStateMonitors.get(i).asBinder() == callback.asBinder()) {
+    private void addStateCallback(IForensicServiceStateCallback callback) throws RemoteException {
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
                 return;
             }
         }
-        mStateMonitors.add(callback);
+        mStateCallbacks.add(callback);
         callback.onStateChange(mState);
     }
 
-    private void notifyStateMonitors() throws RemoteException {
-        for (int i = 0; i < mStateMonitors.size(); i++) {
-            mStateMonitors.get(i).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 makeVisible(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_INVISIBLE:
-                if (!mDataAggregator.initialize()) {
-                    callback.onFailure(ERROR_DATA_SOURCE_UNAVAILABLE);
-                    break;
-                }
-                mState = STATE_VISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_VISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+    private void notifyStateMonitors() {
+        if (mStateCallbacks.size() >= MAX_STATE_CALLBACK_NUM) {
+            mStateCallbacks.removeFirst();
         }
-    }
 
-    private void makeInvisible(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_VISIBLE:
-            case STATE_ENABLED:
-                mState = STATE_INVISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_INVISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        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 {
-        switch (mState) {
-            case STATE_VISIBLE:
-                if (!mBackupTransportConnection.initialize()) {
-                    callback.onFailure(ERROR_BACKUP_TRANSPORT_UNAVAILABLE);
-                    break;
-                }
-                mDataAggregator.enable();
-                mState = STATE_ENABLED;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_ENABLED:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        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 {
-        switch (mState) {
-            case STATE_ENABLED:
-                mBackupTransportConnection.release();
-                mDataAggregator.disable();
-                mState = STATE_VISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_VISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        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_BACKUP, events).sendToTarget();
+        mHandler.obtainMessage(MSG_TRANSPORT, events).sendToTarget();
     }
 
-    private void backup(List<ForensicEvent> events) {
-        mBackupTransportConnection.addData(events);
+    private void transport(List<ForensicEvent> events) {
+        mForensicEventTransportConnection.addData(events);
     }
 
     @Override
@@ -296,9 +278,11 @@
     interface Injector {
         Context getContext();
 
+        PermissionEnforcer getPermissionEnforcer();
+
         Looper getLooper();
 
-        BackupTransportConnection getBackupTransportConnection();
+        ForensicEventTransportConnection getForensicEventransportConnection();
 
         DataAggregator getDataAggregator(ForensicService forensicService);
     }
@@ -315,6 +299,10 @@
             return mContext;
         }
 
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return PermissionEnforcer.fromContext(mContext);
+        }
 
         @Override
         public Looper getLooper() {
@@ -326,8 +314,8 @@
         }
 
         @Override
-        public BackupTransportConnection getBackupTransportConnection() {
-            return new BackupTransportConnection(mContext);
+        public ForensicEventTransportConnection getForensicEventransportConnection() {
+            return new ForensicEventTransportConnection(mContext);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6707a27..f50417d 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2856,7 +2856,10 @@
 
     void prepareForShutdown() {
         for (int i = 0; i < getChildCount(); i++) {
-            createSleepToken("shutdown", getChildAt(i).mDisplayId);
+            final int displayId = getChildAt(i).mDisplayId;
+            mWindowManager.mSnapshotController.mTaskSnapshotController
+                    .snapshotForShutdown(displayId);
+            createSleepToken("shutdown", displayId);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index 1c8c245..bd8e8f4 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -64,6 +64,7 @@
     private boolean mStarted;
     private final Object mLock = new Object();
     private final UserManagerInternal mUserManagerInternal;
+    private boolean mShutdown;
 
     SnapshotPersistQueue() {
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -101,6 +102,16 @@
         }
     }
 
+    /**
+     * Write out everything in the queue because of shutdown.
+     */
+    void shutdown() {
+        synchronized (mLock) {
+            mShutdown = true;
+            mLock.notifyAll();
+        }
+    }
+
     @VisibleForTesting
     void waitForQueueEmpty() {
         while (true) {
@@ -193,7 +204,9 @@
                     if (isReadyToWrite) {
                         next.write();
                     }
-                    SystemClock.sleep(DELAY_MS);
+                    if (!mShutdown) {
+                        SystemClock.sleep(DELAY_MS);
+                    }
                 }
                 synchronized (mLock) {
                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 1f82cdb..9fe3f756 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -307,6 +307,28 @@
     }
 
     /**
+     * Record task snapshots before shutdown.
+     */
+    void snapshotForShutdown(int displayId) {
+        if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) {
+            return;
+        }
+        final DisplayContent displayContent = mService.mRoot.getDisplayContent(displayId);
+        if (displayContent == null) {
+            return;
+        }
+        displayContent.forAllLeafTasks(task -> {
+            if (task.isVisible() && !task.isActivityTypeHome()) {
+                final TaskSnapshot snapshot = captureSnapshot(task);
+                if (snapshot != null) {
+                    mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
+                }
+            }
+        }, true /* traverseTopToBottom */);
+        mPersister.mSnapshotPersistQueue.shutdown();
+    }
+
+    /**
      * Called when screen is being turned off.
      */
     void screenTurningOff(int displayId, ScreenOffListener listener) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ead1282..0918965 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -788,7 +788,9 @@
                 deferResume = false;
                 // Already calls ensureActivityConfig
                 mService.mRootWindowContainer.ensureActivitiesVisible();
-                mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
+                if (!mService.mRootWindowContainer.resumeFocusedTasksTopActivities()) {
+                    mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT-effects");
+                }
             } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
                 for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
                     haveConfigChanges.valueAt(i).forAllActivities(r -> {
@@ -886,7 +888,8 @@
 
         if (windowingMode > -1) {
             if (mService.isInLockTaskMode()
-                    && WindowConfiguration.inMultiWindowMode(windowingMode)) {
+                    && WindowConfiguration.inMultiWindowMode(windowingMode)
+                    && !container.isEmbedded()) {
                 Slog.w(TAG, "Dropping unsupported request to set multi-window windowing mode"
                         + " during locked task mode.");
                 return effects;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index b982098..76d16e1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -371,6 +371,9 @@
     }
 
     ActiveAdmin(int userId, boolean permissionBased) {
+        if (Flags.activeAdminCleanup()) {
+            throw new UnsupportedOperationException("permission based admin no longer supported");
+        }
         if (permissionBased == false) {
             throw new IllegalArgumentException("Can only pass true for permissionBased admin");
         }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
index 395ea91..c937e10 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
@@ -21,6 +21,7 @@
 import android.annotation.UserIdInt;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.content.ComponentName;
 import android.os.FileUtils;
 import android.os.PersistableBundle;
@@ -124,17 +125,18 @@
     final ArrayList<ActiveAdmin> mAdminList = new ArrayList<>();
     final ArrayList<ComponentName> mRemovingAdmins = new ArrayList<>();
 
-    // Some DevicePolicyManager APIs can be called by (1) a DPC or (2) an app with permissions that
-    // isn't a DPC. For the latter, the caller won't have to provide a ComponentName and won't be
-    // mapped to an ActiveAdmin. This permission-based admin should be used to persist policies
-    // set by the permission-based caller. This admin should not be added to mAdminMap or mAdminList
-    // since a lot of methods in DPMS assume the ActiveAdmins here have a valid ComponentName.
-    // Instead, use variants of DPMS active admin getters to include the permission-based admin.
+    /**
+     * @deprecated Do not use. Policies set by permission holders must go into DevicePolicyEngine.
+     */
+    @Deprecated
     ActiveAdmin mPermissionBasedAdmin;
 
     // Create or get the permission-based admin. The permission-based admin will not have a
     // DeviceAdminInfo or ComponentName.
     ActiveAdmin createOrGetPermissionBasedAdmin(int userId) {
+        if (Flags.activeAdminCleanup()) {
+            throw new UnsupportedOperationException("permission based admin no longer supported");
+        }
         if (mPermissionBasedAdmin == null) {
             mPermissionBasedAdmin = new ActiveAdmin(userId, /* permissionBased= */ true);
         }
@@ -147,7 +149,7 @@
     // This is the list of component allowed to start lock task mode.
     List<String> mLockTaskPackages = new ArrayList<>();
 
-    /** @deprecated moved to {@link ActiveAdmin#protectedPackages}. */
+    /** @deprecated moved to DevicePolicyEngine. */
     @Deprecated
     @Nullable
     List<String> mUserControlDisabledPackages;
@@ -280,7 +282,7 @@
                 }
             }
 
-            if (policyData.mPermissionBasedAdmin != null) {
+            if (!Flags.activeAdminCleanup() && policyData.mPermissionBasedAdmin != null) {
                 out.startTag(null, "permission-based-admin");
                 policyData.mPermissionBasedAdmin.writeToXml(out);
                 out.endTag(null, "permission-based-admin");
@@ -521,7 +523,8 @@
                     } catch (RuntimeException e) {
                         Slogf.w(TAG, e, "Failed loading admin %s", name);
                     }
-                } else if ("permission-based-admin".equals(tag)) {
+                } else if (!Flags.activeAdminCleanup() && "permission-based-admin".equals(tag)) {
+
                     ActiveAdmin ap = new ActiveAdmin(policy.mUserId, /* permissionBased= */ true);
                     ap.readFromXml(parser, /* overwritePolicies= */ false);
                     policy.mPermissionBasedAdmin = ap;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index dff3438..ad7e21c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -376,6 +376,7 @@
 import android.app.compat.CompatChanges;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.trust.TrustManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.compat.annotation.ChangeId;
@@ -926,6 +927,8 @@
     final UsageStatsManagerInternal mUsageStatsManagerInternal;
     final TelephonyManager mTelephonyManager;
     final RoleManager mRoleManager;
+    final SupervisionManagerInternal mSupervisionManagerInternal;
+
     private final LockPatternUtils mLockPatternUtils;
     private final LockSettingsInternal mLockSettingsInternal;
     private final DeviceAdminServiceController mDeviceAdminServiceController;
@@ -2082,6 +2085,11 @@
         boolean isAdminInstalledCaCertAutoApproved() {
             return false;
         }
+
+        @Nullable
+        SupervisionManagerInternal getSupervisionManager() {
+            return LocalServices.getService(SupervisionManagerInternal.class);
+        }
     }
 
     /**
@@ -2113,6 +2121,11 @@
         mIPermissionManager = Objects.requireNonNull(injector.getIPermissionManager());
         mTelephonyManager = Objects.requireNonNull(injector.getTelephonyManager());
         mRoleManager = Objects.requireNonNull(injector.getRoleManager());
+        if (Flags.secondaryLockscreenApiEnabled()) {
+            mSupervisionManagerInternal = injector.getSupervisionManager();
+        } else {
+            mSupervisionManagerInternal = null;
+        }
 
         mLocalService = new LocalService();
         mLockPatternUtils = injector.newLockPatternUtils();
@@ -2234,7 +2247,6 @@
         return Collections.unmodifiableSet(packageNames);
     }
 
-
     private @Nullable String getDefaultRoleHolderPackageName(int resId) {
         String packageNameAndSignature = mContext.getString(resId);
 
@@ -3966,7 +3978,8 @@
             final int N = admins.size();
             for (int i = 0; i < N; i++) {
                 ActiveAdmin admin = admins.get(i);
-                if ((admin.isPermissionBased || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD))
+                if (((!Flags.activeAdminCleanup() && admin.isPermissionBased)
+                        || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD))
                         && admin.passwordExpirationTimeout > 0L
                         && now >= admin.passwordExpirationDate - EXPIRATION_GRACE_PERIOD_MS
                         && admin.passwordExpirationDate > 0L) {
@@ -5563,13 +5576,25 @@
                 caller.getUserId());
         Preconditions.checkArgument(!calledOnParent || isProfileOwner(caller));
 
-        ActiveAdmin activeAdmin = admin.getActiveAdmin();
+        final ActiveAdmin activeAdmin;
+        if (Flags.activeAdminCleanup()) {
+            if (admin.hasAuthority(EnforcingAdmin.DPC_AUTHORITY)) {
+                synchronized (getLockObject()) {
+                    activeAdmin = getActiveAdminUncheckedLocked(
+                            admin.getComponentName(), admin.getUserId());
+                }
+            } else {
+                activeAdmin = null;
+            }
+        } else {
+            activeAdmin = admin.getActiveAdmin();
+        }
 
         // We require the caller to explicitly clear any password quality requirements set
         // on the parent DPM instance, to avoid the case where password requirements are
         // specified in the form of quality on the parent but complexity on the profile
         // itself.
-        if (!calledOnParent) {
+        if (activeAdmin != null && !calledOnParent) {
             final boolean hasQualityRequirementsOnParent = activeAdmin.hasParentActiveAdmin()
                     && activeAdmin.getParentActiveAdmin().mPasswordPolicy.quality
                     != PASSWORD_QUALITY_UNSPECIFIED;
@@ -5593,20 +5618,22 @@
         }
 
         mInjector.binderWithCleanCallingIdentity(() -> {
-            // Reset the password policy.
-            if (calledOnParent) {
-                activeAdmin.getParentActiveAdmin().mPasswordPolicy = new PasswordPolicy();
-            } else {
-                activeAdmin.mPasswordPolicy = new PasswordPolicy();
+            if (activeAdmin != null) {
+                // Reset the password policy.
+                if (calledOnParent) {
+                    activeAdmin.getParentActiveAdmin().mPasswordPolicy = new PasswordPolicy();
+                } else {
+                    activeAdmin.mPasswordPolicy = new PasswordPolicy();
+                }
+                updatePasswordQualityCacheForUserGroup(caller.getUserId());
             }
+
             synchronized (getLockObject()) {
                 updatePasswordValidityCheckpointLocked(caller.getUserId(), calledOnParent);
             }
-            updatePasswordQualityCacheForUserGroup(caller.getUserId());
             saveSettingsLocked(caller.getUserId());
         });
 
-
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_PASSWORD_COMPLEXITY)
                 .setAdmin(caller.getPackageName())
@@ -6287,28 +6314,33 @@
         final int callingUserId = caller.getUserId();
         ComponentName adminComponent = null;
         synchronized (getLockObject()) {
-            ActiveAdmin admin;
             // Make sure the caller has any active admin with the right policy or
             // the required permission.
             if (Flags.lockNowCoexistence()) {
-                admin = enforcePermissionsAndGetEnforcingAdmin(
+                EnforcingAdmin enforcingAdmin = enforcePermissionsAndGetEnforcingAdmin(
                         /* admin= */ null,
                         /* permissions= */ new String[]{MANAGE_DEVICE_POLICY_LOCK, LOCK_DEVICE},
                         /* deviceAdminPolicy= */ USES_POLICY_FORCE_LOCK,
                         caller.getPackageName(),
                         getAffectedUser(parent)
-                 ).getActiveAdmin();
+                );
+                if (Flags.activeAdminCleanup()) {
+                    adminComponent = enforcingAdmin.getComponentName();
+                } else {
+                    ActiveAdmin admin = enforcingAdmin.getActiveAdmin();
+                    adminComponent = admin == null ? null : admin.info.getComponent();
+                }
             } else {
-                admin = getActiveAdminOrCheckPermissionForCallerLocked(
+                ActiveAdmin admin = getActiveAdminOrCheckPermissionForCallerLocked(
                         null,
                         DeviceAdminInfo.USES_POLICY_FORCE_LOCK,
                         parent,
                         LOCK_DEVICE);
+                adminComponent = admin == null ? null : admin.info.getComponent();
             }
             checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_LOCK_NOW);
             final long ident = mInjector.binderClearCallingIdentity();
             try {
-                adminComponent = admin == null ? null : admin.info.getComponent();
                 if (adminComponent != null) {
                     // For Profile Owners only, callers with only permission not allowed.
                     if ((flags & DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY) != 0) {
@@ -7777,7 +7809,6 @@
                 USES_POLICY_WIPE_DATA,
                 caller.getPackageName(),
                 factoryReset ? UserHandle.USER_ALL : getAffectedUser(calledOnParentInstance));
-        ActiveAdmin admin = enforcingAdmin.getActiveAdmin();
 
         checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_WIPE_DATA);
 
@@ -7786,10 +7817,20 @@
                     calledByProfileOwnerOnOrgOwnedDevice, calledOnParentInstance);
         }
 
-        int userId = admin != null ? admin.getUserHandle().getIdentifier()
-                : caller.getUserId();
-        Slogf.i(LOG_TAG, "wipeDataWithReason(%s): admin=%s, user=%d", wipeReasonForUser, admin,
-                userId);
+        int userId;
+        ActiveAdmin admin = null;
+        if (Flags.activeAdminCleanup()) {
+            userId = enforcingAdmin.getUserId();
+            Slogf.i(LOG_TAG, "wipeDataWithReason(%s): admin=%s, user=%d", wipeReasonForUser,
+                    enforcingAdmin, userId);
+        } else {
+            admin = enforcingAdmin.getActiveAdmin();
+            userId = admin != null ? admin.getUserHandle().getIdentifier()
+                    : caller.getUserId();
+            Slogf.i(LOG_TAG, "wipeDataWithReason(%s): admin=%s, user=%d", wipeReasonForUser, admin,
+                    userId);
+        }
+
         if (calledByProfileOwnerOnOrgOwnedDevice) {
             // When wipeData is called on the parent instance, it implies wiping the entire device.
             if (calledOnParentInstance) {
@@ -7810,25 +7851,36 @@
 
         final String adminName;
         final ComponentName adminComp;
-        if (admin != null) {
-            if (admin.isPermissionBased) {
-                adminComp = null;
-                adminName = caller.getPackageName();
-                event.setAdmin(adminName);
-            } else {
-                adminComp = admin.info.getComponent();
-                adminName = adminComp.flattenToShortString();
-                event.setAdmin(adminComp);
-            }
+        if (Flags.activeAdminCleanup()) {
+            adminComp = enforcingAdmin.getComponentName();
+            adminName = adminComp != null
+                    ? adminComp.flattenToShortString()
+                    : enforcingAdmin.getPackageName();
+            event.setAdmin(enforcingAdmin.getPackageName());
+            // Not including any HSUM handling here because the "else" branch in the "flag off"
+            // case below is unreachable under normal circumstances and for permission-based
+            // callers admin won't be null.
         } else {
-            adminComp = null;
-            adminName = mInjector.getPackageManager().getPackagesForUid(caller.getUid())[0];
-            Slogf.i(LOG_TAG, "Logging wipeData() event admin as " + adminName);
-            event.setAdmin(adminName);
-            if (mInjector.userManagerIsHeadlessSystemUserMode()) {
-                // On headless system user mode, the call is meant to factory reset the whole
-                // device, otherwise the caller could simply remove the current user.
-                userId = UserHandle.USER_SYSTEM;
+            if (admin != null) {
+                if (admin.isPermissionBased) {
+                    adminComp = null;
+                    adminName = caller.getPackageName();
+                    event.setAdmin(adminName);
+                } else {
+                    adminComp = admin.info.getComponent();
+                    adminName = adminComp.flattenToShortString();
+                    event.setAdmin(adminComp);
+                }
+            } else {
+                adminComp = null;
+                adminName = mInjector.getPackageManager().getPackagesForUid(caller.getUid())[0];
+                Slogf.i(LOG_TAG, "Logging wipeData() event admin as " + adminName);
+                event.setAdmin(adminName);
+                if (mInjector.userManagerIsHeadlessSystemUserMode()) {
+                    // On headless system user mode, the call is meant to factory reset the whole
+                    // device, otherwise the caller could simply remove the current user.
+                    userId = UserHandle.USER_SYSTEM;
+                }
             }
         }
         event.write();
@@ -8316,7 +8368,8 @@
         List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(userHandle);
         for (int i = 0; i < admins.size(); i++) {
             ActiveAdmin admin = admins.get(i);
-            if (admin.isPermissionBased || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)) {
+            if ((!Flags.activeAdminCleanup() && admin.isPermissionBased)
+                    || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)) {
                 affectedUserIds.add(admin.getUserHandle().getIdentifier());
                 long timeout = admin.passwordExpirationTimeout;
                 admin.passwordExpirationDate =
@@ -8410,7 +8463,7 @@
      */
     private int getUserIdToWipeForFailedPasswords(ActiveAdmin admin) {
         final int userId = admin.getUserHandle().getIdentifier();
-        if (admin.isPermissionBased) {
+        if (!Flags.activeAdminCleanup() && admin.isPermissionBased) {
             return userId;
         }
         final ComponentName component = admin.info.getComponent();
@@ -14585,34 +14638,76 @@
         }
     }
 
+    private boolean hasActiveSupervisionTestAdminLocked(@UserIdInt int userId) {
+        ensureLocked();
+        if (mConstants.USE_TEST_ADMIN_AS_SUPERVISION_COMPONENT) {
+            final DevicePolicyData policy = getUserData(userId);
+            for (ActiveAdmin admin : policy.mAdminMap.values()) {
+                if (admin != null && admin.testOnlyAdmin) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     @Override
     public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled,
             PersistableBundle options) {
-        Objects.requireNonNull(who, "ComponentName is null");
-
-        // Check can set secondary lockscreen enabled
-        final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isProfileOwner(caller));
-        Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()),
-                "User %d is not allowed to call setSecondaryLockscreenEnabled",
+        if (Flags.secondaryLockscreenApiEnabled()) {
+            final CallerIdentity caller = getCallerIdentity();
+            final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller);
+            synchronized (getLockObject()) {
+                // TODO(b/378102594): Remove access for test admins.
+                final boolean isTestAdmin = hasActiveSupervisionTestAdminLocked(caller.getUserId());
+                Preconditions.checkCallAuthorization(isRoleHolder || isTestAdmin,
+                        "Caller (%d) is not the SYSTEM_SUPERVISION role holder",
                         caller.getUserId());
+            }
 
-        synchronized (getLockObject()) {
-            // Allow testOnly admins to bypass supervision config requirement.
-            Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId())
-                    || isSupervisionComponentLocked(caller.getComponentName()), "Admin %s is not "
-                    + "the default supervision component", caller.getComponentName());
-            DevicePolicyData policy = getUserData(caller.getUserId());
-            policy.mSecondaryLockscreenEnabled = enabled;
-            saveSettingsLocked(caller.getUserId());
+            if (mSupervisionManagerInternal != null) {
+                mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
+                        caller.getUserId(), enabled, options);
+            } else {
+                synchronized (getLockObject()) {
+                    DevicePolicyData policy = getUserData(caller.getUserId());
+                    policy.mSecondaryLockscreenEnabled = enabled;
+                    saveSettingsLocked(caller.getUserId());
+                }
+            }
+        } else {
+            Objects.requireNonNull(who, "ComponentName is null");
+
+            // Check can set secondary lockscreen enabled
+            final CallerIdentity caller = getCallerIdentity(who);
+            Preconditions.checkCallAuthorization(
+                    isDefaultDeviceOwner(caller) || isProfileOwner(caller));
+            Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()),
+                    "User %d is not allowed to call setSecondaryLockscreenEnabled",
+                            caller.getUserId());
+
+            synchronized (getLockObject()) {
+                // Allow testOnly admins to bypass supervision config requirement.
+                Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId())
+                        || isSupervisionComponentLocked(caller.getComponentName()),
+                        "Admin %s is not the default supervision component",
+                        caller.getComponentName());
+                DevicePolicyData policy = getUserData(caller.getUserId());
+                policy.mSecondaryLockscreenEnabled = enabled;
+                saveSettingsLocked(caller.getUserId());
+            }
         }
     }
 
     @Override
     public boolean isSecondaryLockscreenEnabled(@NonNull UserHandle userHandle) {
-        synchronized (getLockObject()) {
-            return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled;
+        if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) {
+            return mSupervisionManagerInternal.isSupervisionLockscreenEnabledForUser(
+                    userHandle.getIdentifier());
+        } else {
+            synchronized (getLockObject()) {
+                return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled;
+            }
         }
     }
 
@@ -16272,7 +16367,8 @@
         if (admin.mPasswordPolicy.quality < minPasswordQuality) {
             return false;
         }
-        return admin.isPermissionBased || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD);
+        return (!Flags.activeAdminCleanup() && admin.isPermissionBased)
+                || admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD);
     }
 
     @Override
@@ -19148,6 +19244,14 @@
         }
     }
 
+    private boolean isAnyResetPasswordTokenActiveForUser(int userId) {
+        return mDevicePolicyEngine
+                .getLocalPoliciesSetByAdmins(PolicyDefinition.RESET_PASSWORD_TOKEN, userId)
+                .values()
+                .stream()
+                .anyMatch((p) -> isResetPasswordTokenActiveForUserLocked(p.getValue(), userId));
+    }
+
     private boolean isResetPasswordTokenActiveForUserLocked(
             long passwordTokenHandle, int userHandle) {
         if (passwordTokenHandle != 0) {
@@ -21003,6 +21107,9 @@
         Preconditions.checkCallAuthorization(isSystemUid(getCallerIdentity()),
                 String.format(NOT_SYSTEM_CALLER_MSG,
                         "call canProfileOwnerResetPasswordWhenLocked"));
+        if (Flags.resetPasswordWithTokenCoexistence()) {
+            return isAnyResetPasswordTokenActiveForUser(userId);
+        }
         synchronized (getLockObject()) {
             final ActiveAdmin poAdmin = getProfileOwnerAdminLocked(userId);
             DevicePolicyData policy = getUserData(userId);
@@ -23345,7 +23452,8 @@
             return EnforcingAdmin.createDeviceAdminEnforcingAdmin(admin.info.getComponent(), userId,
                     admin);
         }
-        admin = getUserData(userId).createOrGetPermissionBasedAdmin(userId);
+        admin = Flags.activeAdminCleanup()
+                ? null : getUserData(userId).createOrGetPermissionBasedAdmin(userId);
         return  EnforcingAdmin.createEnforcingAdmin(caller.getPackageName(), userId, admin);
     }
 
@@ -23368,8 +23476,8 @@
                 }
             }
         }
-
-        admin = getUserData(userId).createOrGetPermissionBasedAdmin(userId);
+        admin = Flags.activeAdminCleanup()
+                ? null : getUserData(userId).createOrGetPermissionBasedAdmin(userId);
         return  EnforcingAdmin.createEnforcingAdmin(packageName, userId, admin);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
index 58e3a7d..1fd628a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
@@ -23,6 +23,7 @@
 import android.app.admin.DpcAuthority;
 import android.app.admin.RoleAuthority;
 import android.app.admin.UnknownAuthority;
+import android.app.admin.flags.Flags;
 import android.content.ComponentName;
 import android.os.UserHandle;
 
@@ -295,9 +296,17 @@
 
     @Nullable
     public ActiveAdmin getActiveAdmin() {
+        if (Flags.activeAdminCleanup()) {
+            throw new UnsupportedOperationException("getActiveAdmin() no longer supported");
+        }
         return mActiveAdmin;
     }
 
+    @Nullable
+    ComponentName getComponentName() {
+        return mComponentName;
+    }
+
     @NonNull
     android.app.admin.EnforcingAdmin getParcelableAdmin() {
         Authority authority;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 19b0343..3e7c4ef 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -251,6 +251,7 @@
 import com.android.server.security.KeyChainSystemService;
 import com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService;
 import com.android.server.security.advancedprotection.AdvancedProtectionService;
+import com.android.server.security.forensic.ForensicService;
 import com.android.server.security.rkp.RemoteProvisioningService;
 import com.android.server.selinux.SelinuxAuditLogsService;
 import com.android.server.sensorprivacy.SensorPrivacyService;
@@ -1760,6 +1761,13 @@
             mSystemServiceManager.startService(LogcatManagerService.class);
             t.traceEnd();
 
+            if (!isWatch && !isTv && !isAutomotive
+                    && android.security.Flags.aflApi()) {
+                t.traceBegin("StartForensicService");
+                mSystemServiceManager.startService(ForensicService.class);
+                t.traceEnd();
+            }
+
             if (AppFunctionManagerConfiguration.isSupported(context)) {
                 t.traceBegin("StartAppFunctionManager");
                 mSystemServiceManager.startService(AppFunctionManagerService.class);
@@ -2761,8 +2769,9 @@
             mSystemServiceManager.startService(WEAR_MODE_SERVICE_CLASS);
             t.traceEnd();
 
-            boolean enableWristOrientationService = SystemProperties.getBoolean(
-                    "config.enable_wristorientation", false);
+            boolean enableWristOrientationService =
+                    !android.server.Flags.migrateWristOrientation()
+                    && SystemProperties.getBoolean("config.enable_wristorientation", false);
             if (enableWristOrientationService) {
                 t.traceBegin("StartWristOrientationService");
                 mSystemServiceManager.startService(WRIST_ORIENTATION_SERVICE_CLASS);
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index e2ac22d..4412968 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -39,6 +39,14 @@
 }
 
 flag {
+     name: "migrate_wrist_orientation"
+     namespace: "wear_frameworks"
+     description: "Migrate wrist orientation service functionality to wear settings service"
+     bug: "352725980"
+     is_fixed_read_only: true
+}
+
+flag {
     name: "allow_network_time_update_service"
     namespace: "wear_systems"
     description: "Allow NetworkTimeUpdateService on Wear"
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 67e2547..53a25dd 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -21,10 +21,11 @@
 import android.annotation.UserIdInt;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.supervision.ISupervisionManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.UserInfo;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -179,8 +180,15 @@
         }
 
         @Override
+        public boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId) {
+            synchronized (getLockObject()) {
+                return getUserDataLocked(userId).supervisionLockScreenEnabled;
+            }
+        }
+
+        @Override
         public void setSupervisionLockscreenEnabledForUser(
-                @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+                @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options) {
             synchronized (getLockObject()) {
                 SupervisionUserData data = getUserDataLocked(userId);
                 data.supervisionLockScreenEnabled = enabled;
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
index 5616237..1dd48f5 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -19,7 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.util.IndentingPrintWriter;
 
 /** User specific data, used internally by the {@link SupervisionService}. */
@@ -27,7 +27,7 @@
     public final @UserIdInt int userId;
     public boolean supervisionEnabled;
     public boolean supervisionLockScreenEnabled;
-    @Nullable public Bundle supervisionLockScreenOptions;
+    @Nullable public PersistableBundle supervisionLockScreenOptions;
 
     public SupervisionUserData(@UserIdInt int userId) {
         this.userId = userId;
diff --git a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
index 08155c7..9772ef9 100644
--- a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
+++ b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
@@ -2380,10 +2380,14 @@
     @Test
     public void doTestMigrateIkeSession_Vcn() throws Exception {
         final int expectedKeepalive = 2097; // Any unlikely number will do
-        final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
-                .build();
+        final NetworkCapabilities vcnNc =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(TRANSPORT_CELLULAR)
+                        .setTransportInfo(
+                                new VcnTransportInfo.Builder()
+                                        .setMinUdpPort4500NatTimeoutSeconds(expectedKeepalive)
+                                        .build())
+                        .build();
         final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
                 true /* isAutomaticIpVersionSelectionEnabled */,
                 true /* isAutomaticNattKeepaliveTimerEnabled */,
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 30de0e8..8dc8c14 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -67,7 +67,6 @@
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REFRESH_EXACT_ALARM_CANDIDATES;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS;
-import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.TEMPORARY_QUOTA_CHANGED;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA;
@@ -152,7 +151,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.platform.test.flag.util.FlagSetException;
@@ -436,8 +434,7 @@
      */
     private void disableFlagsNotSetByAnnotation() {
         try {
-            mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
-                    Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS);
+            mSetFlagsRule.disableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS);
         } catch (FlagSetException fse) {
             // Expected if the test about to be run requires this enabled.
         }
@@ -523,13 +520,11 @@
 
         mService.onStart();
 
-        if (Flags.useFrozenStateToDropListenerAlarms()) {
-            final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor =
-                    ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class);
-            verify(mActivityManager).registerUidFrozenStateChangedCallback(
-                    any(HandlerExecutor.class), frozenCaptor.capture());
-            mUidFrozenStateCallback = frozenCaptor.getValue();
-        }
+        final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor =
+                ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class);
+        verify(mActivityManager).registerUidFrozenStateChangedCallback(
+                any(HandlerExecutor.class), frozenCaptor.capture());
+        mUidFrozenStateCallback = frozenCaptor.getValue();
 
         // Unable to mock mMockContext to return a mock stats manager.
         // So just mocking the whole MetricsHelper instance.
@@ -3744,79 +3739,11 @@
         testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_RARE);
     }
 
-    @Test
-    public void exactListenerAlarmsRemovedOnCached() {
-        mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
-
-        setTestAlarmWithListener(ELAPSED_REALTIME, 31, getNewListener(() -> {}), WINDOW_EXACT,
-                TEST_CALLING_UID);
-        setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
-        setTestAlarm(ELAPSED_REALTIME, 54, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
-                TEST_CALLING_UID, null);
-        setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
-
-        setTestAlarmWithListener(ELAPSED_REALTIME, 21, getNewListener(() -> {}), WINDOW_EXACT,
-                TEST_CALLING_UID_2);
-        setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
-        setTestAlarm(ELAPSED_REALTIME, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
-                TEST_CALLING_UID_2, null);
-        setTestAlarm(RTC, 549, 234, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID_2, null);
-
-        assertEquals(8, mService.mAlarmStore.size());
-
-        mListener.handleUidCachedChanged(TEST_CALLING_UID, true);
-        assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
-        assertEquals(7, mService.mAlarmStore.size());
-
-        mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true);
-        assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
-        assertEquals(6, mService.mAlarmStore.size());
-    }
-
-    @Test
-    public void alarmCountOnListenerCached() {
-        mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
-
-        // Set some alarms for TEST_CALLING_UID.
-        final int numExactListenerUid1 = 14;
-        for (int i = 0; i < numExactListenerUid1; i++) {
-            setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
-                    getNewListener(() -> {}));
-        }
-        setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
-        setTestAlarm(ELAPSED_REALTIME, 54, getNewMockPendingIntent());
-        setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
-
-        // Set some alarms for TEST_CALLING_UID_2.
-        final int numExactListenerUid2 = 9;
-        for (int i = 0; i < numExactListenerUid2; i++) {
-            setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
-                    getNewListener(() -> {}), WINDOW_EXACT, TEST_CALLING_UID_2);
-        }
-        setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
-        setTestAlarm(RTC_WAKEUP, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
-                TEST_CALLING_UID_2, null);
-
-        assertEquals(numExactListenerUid1 + 3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
-        assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
-
-        mListener.handleUidCachedChanged(TEST_CALLING_UID, true);
-        assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
-        assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
-        assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
-
-        mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true);
-        assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
-        assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
-        assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
-    }
-
     private void executeUidFrozenStateCallback(int[] uids, int[] frozenStates) {
         assertNotNull(mUidFrozenStateCallback);
         mUidFrozenStateCallback.onUidFrozenStateChanged(uids, frozenStates);
     }
 
-    @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
     @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
     @Test
     public void exactListenerAlarmsRemovedOnFrozen() {
@@ -3848,7 +3775,6 @@
         assertEquals(6, mService.mAlarmStore.size());
     }
 
-    @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
     @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
     @Test
     public void alarmCountOnListenerFrozen() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index a1a8b0e..1caa02a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -185,10 +185,10 @@
 
         doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
 
+        doReturn(true).when(mPlatformCompat).isChangeEnabledInternalNoLogging(
+                eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class));
         doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
-        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     public void tearDown() throws Exception {
@@ -298,7 +298,7 @@
         filter.setPriority(priority);
         final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
                 receiverList.app.info.packageName, null, null, null, receiverList.uid,
-                receiverList.userId, false, false, true,
+                receiverList.userId, false, false, true, receiverList.app.info,
                 mock(PlatformCompat.class));
         receiverList.add(res);
         return res;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java
index e977a7d..5d106ac 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java
@@ -20,10 +20,13 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 
+import android.content.pm.ApplicationInfo;
 import android.os.Process;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
@@ -53,14 +56,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+
+        doReturn(true).when(mPlatformCompat).isChangeEnabledInternalNoLogging(
+                eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class));
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES)
     public void testCalculateAdjustedPriority() {
-        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
-
         {
             // Pairs of {initial-priority, expected-adjusted-priority}
             final Pair<Integer, Integer>[] priorities = new Pair[] {
@@ -95,8 +98,8 @@
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES)
     public void testCalculateAdjustedPriority_withChangeIdDisabled() {
-        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
+        doReturn(false).when(mPlatformCompat).isChangeEnabledInternalNoLogging(
+                eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class));
 
         {
             // Pairs of {initial-priority, expected-adjusted-priority}
@@ -132,9 +135,6 @@
     @Test
     @DisableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES)
     public void testCalculateAdjustedPriority_withFlagDisabled() {
-        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
-
         {
             // Pairs of {initial-priority, expected-adjusted-priority}
             final Pair<Integer, Integer>[] priorities = new Pair[] {
@@ -170,7 +170,7 @@
     @DisableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES)
     public void testCalculateAdjustedPriority_withFlagDisabled_withChangeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
+                eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), anyInt());
 
         {
             // Pairs of {initial-priority, expected-adjusted-priority}
@@ -215,6 +215,7 @@
         final String errorMsg = String.format("owner=%d; actualPriority=%d; expectedPriority=%d",
                 owningUid, priority, expectedAdjustedPriority);
         assertWithMessage(errorMsg).that(BroadcastFilter.calculateAdjustedPriority(
-                owningUid, priority, mPlatformCompat)).isEqualTo(expectedAdjustedPriority);
+                owningUid, priority, mock(ApplicationInfo.class), mPlatformCompat))
+                        .isEqualTo(expectedAdjustedPriority);
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 1481085..88caaa6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -717,7 +717,7 @@
     @Test
     public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE),
                 eq(getUidForPackage(PACKAGE_GREEN)));
         final List receivers = List.of(
                 withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
@@ -1289,7 +1289,7 @@
     @Test
     public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE),
                 eq(getUidForPackage(PACKAGE_GREEN)));
 
         final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
@@ -1824,7 +1824,7 @@
     @Test
     public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE),
                 eq(getUidForPackage(PACKAGE_GREEN)));
 
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
@@ -2028,7 +2028,7 @@
     public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled()
             throws Exception {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE),
                 eq(getUidForPackage(PACKAGE_GREEN)));
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
         final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 9d92d5f..a38ef78 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -1660,7 +1660,7 @@
         final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
 
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
 
         // Enqueue a normal broadcast that will go to several processes, and
         // then enqueue a foreground broadcast that risks reordering
@@ -2472,7 +2472,7 @@
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
 
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
 
         mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
                 ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
index a424bfdb..f9f3790 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -19,7 +19,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE;
+import static com.android.server.am.BroadcastRecord.LIMIT_PRIORITY_SCOPE;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING;
@@ -109,7 +109,7 @@
         MockitoAnnotations.initMocks(this);
 
         doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     @Test
@@ -223,7 +223,7 @@
     @Test
     public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
 
         assertTrue(isPrioritized(List.of(
                 createResolveInfo(PACKAGE1, getAppId(1), 10),
@@ -257,7 +257,7 @@
     @Test
     public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
 
         assertTrue(isPrioritized(List.of(
                 createResolveInfo(PACKAGE1, getAppId(1), 10),
@@ -295,7 +295,7 @@
     @Test
     public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
 
         assertTrue(isPrioritized(List.of(
                 createResolveInfo(PACKAGE1, getAppId(1), 10),
@@ -329,9 +329,9 @@
     @Test
     public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
 
         assertTrue(isPrioritized(List.of(
                 createResolveInfo(PACKAGE1, getAppId(1), 10),
@@ -593,7 +593,7 @@
     @Test
     public void testSetDeliveryState_DeferUntilActive_changeIdDisabled() {
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
         final BroadcastRecord r = createBroadcastRecord(
                 new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 10),
@@ -961,7 +961,7 @@
                         createResolveInfo(PACKAGE3, getAppId(3)))));
 
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
         assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState(
                 List.of(createResolveInfo(PACKAGE1, getAppId(1)),
                         createResolveInfo(PACKAGE2, getAppId(2)),
@@ -973,7 +973,7 @@
                         createResolveInfo(PACKAGE3, getAppId(3)))));
 
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
         assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState(
                 List.of(createResolveInfo(PACKAGE1, getAppId(1)),
                         createResolveInfo(PACKAGE2, getAppId(2)),
@@ -988,7 +988,7 @@
                                 createResolveInfo(PACKAGE3, getAppId(3)))));
 
         doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
-                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+                eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
         assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState(
                 List.of(createResolveInfo(PACKAGE1, getAppId(1)),
                         createResolveInfo(PACKAGE2, getAppId(2)),
@@ -1005,7 +1005,7 @@
 
     private boolean[] calculateChangeState(List<Object> receivers) {
         return BroadcastRecord.calculateChangeStateForReceivers(receivers,
-                CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat);
+                LIMIT_PRIORITY_SCOPE, mPlatformCompat);
     }
 
     private static void cleanupDisabledPackageReceivers(BroadcastRecord record,
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 58489f3..0881b4c 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 
 import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS;
+import static com.android.server.power.hint.HintManagerService.DEFAULT_HEADROOM_PID;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -51,11 +52,15 @@
 import android.hardware.common.fmq.MQDescriptor;
 import android.hardware.power.ChannelConfig;
 import android.hardware.power.ChannelMessage;
+import android.hardware.power.CpuHeadroomParams;
+import android.hardware.power.GpuHeadroomParams;
 import android.hardware.power.IPower;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
 import android.os.Binder;
+import android.os.CpuHeadroomParamsInternal;
+import android.os.GpuHeadroomParamsInternal;
 import android.os.IBinder;
 import android.os.IHintSession;
 import android.os.PerformanceHintManager;
@@ -128,11 +133,11 @@
     private static final long[] TIMESTAMPS_ZERO = new long[] {};
     private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L};
     private static final WorkDuration[] WORK_DURATIONS_FIVE = new WorkDuration[] {
-        makeWorkDuration(1L, 11L, 1L, 8L, 4L),
-        makeWorkDuration(2L, 13L, 2L, 8L, 6L),
-        makeWorkDuration(3L, 333333333L, 3L, 8L, 333333333L),
-        makeWorkDuration(2L, 13L, 2L, 0L, 6L),
-        makeWorkDuration(2L, 13L, 2L, 8L, 0L),
+            makeWorkDuration(1L, 11L, 1L, 8L, 4L),
+            makeWorkDuration(2L, 13L, 2L, 8L, 6L),
+            makeWorkDuration(3L, 333333333L, 3L, 8L, 333333333L),
+            makeWorkDuration(2L, 13L, 2L, 0L, 6L),
+            makeWorkDuration(2L, 13L, 2L, 8L, 0L),
     };
     private static final String TEST_APP_NAME = "com.android.test.app";
 
@@ -187,17 +192,17 @@
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
                 eq(SESSION_TIDS_A), eq(DEFAULT_TARGET_DURATION), anyInt(),
                 any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[0],
-                    SESSION_IDS[0]));
+                SESSION_IDS[0]));
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
                 eq(SESSION_TIDS_B), eq(DOUBLED_TARGET_DURATION), anyInt(),
                 any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[1],
-                    SESSION_IDS[1]));
+                SESSION_IDS[1]));
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
                 eq(SESSION_TIDS_C), eq(0L), anyInt(),
                 any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2],
-                    SESSION_IDS[2]));
+                SESSION_IDS[2]));
 
-        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(6);
         when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
@@ -217,8 +222,8 @@
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
                 eq(0L))).thenReturn(SESSION_PTRS[2]);
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(anyInt(), anyInt(),
-            any(int[].class), anyLong(), anyInt(),
-            any(SessionConfig.class))).thenThrow(new UnsupportedOperationException());
+                any(int[].class), anyLong(), anyInt(),
+                any(SessionConfig.class))).thenThrow(new UnsupportedOperationException());
     }
 
     static class NativeWrapperFake extends NativeWrapper {
@@ -337,7 +342,7 @@
                 SESSION_TIDS_C, 0L, SessionTag.OTHER, new SessionConfig());
         assertNotNull(c);
         verify(mNativeWrapperMock, times(3)).halCreateHintSession(anyInt(), anyInt(),
-                                                                  any(int[].class), anyLong());
+                any(int[].class), anyLong());
     }
 
     @Test
@@ -487,7 +492,7 @@
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
                 .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                    SessionTag.OTHER, new SessionConfig());
+                        SessionTag.OTHER, new SessionConfig());
 
         a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
         verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(),
@@ -514,7 +519,7 @@
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
                 .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                    SessionTag.OTHER, new SessionConfig());
+                        SessionTag.OTHER, new SessionConfig());
 
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
@@ -1096,4 +1101,157 @@
         verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
         assertTrue(service.hasChannel(TGID, UID));
     }
+
+    @Test
+    public void testHeadroomPowerHalNotSupported() throws Exception {
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
+        HintManagerService service = createService();
+        assertThrows(UnsupportedOperationException.class, () -> {
+            service.getBinderServiceInstance().getCpuHeadroom(null);
+        });
+        assertThrows(UnsupportedOperationException.class, () -> {
+            service.getBinderServiceInstance().getGpuHeadroom(null);
+        });
+        assertThrows(UnsupportedOperationException.class, () -> {
+            service.getBinderServiceInstance().getCpuHeadroomMinIntervalMillis();
+        });
+        assertThrows(UnsupportedOperationException.class, () -> {
+            service.getBinderServiceInstance().getGpuHeadroomMinIntervalMillis();
+        });
+    }
+
+    @Test
+    public void testCpuHeadroomCache() throws Exception {
+        when(mIPowerMock.getCpuHeadroomMinIntervalMillis()).thenReturn(2000L);
+        CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal();
+        CpuHeadroomParams halParams1 = new CpuHeadroomParams();
+        halParams1.calculationType = CpuHeadroomParams.CalculationType.MIN;
+        halParams1.selectionType = CpuHeadroomParams.SelectionType.ALL;
+        halParams1.pid = Process.myPid();
+
+        CpuHeadroomParamsInternal params2 = new CpuHeadroomParamsInternal();
+        params2.usesDeviceHeadroom = true;
+        params2.calculationType = CpuHeadroomParams.CalculationType.AVERAGE;
+        params2.selectionType = CpuHeadroomParams.SelectionType.PER_CORE;
+        CpuHeadroomParams halParams2 = new CpuHeadroomParams();
+        halParams2.calculationType = CpuHeadroomParams.CalculationType.AVERAGE;
+        halParams2.selectionType = CpuHeadroomParams.SelectionType.PER_CORE;
+        halParams2.pid = DEFAULT_HEADROOM_PID;
+
+        float[] headroom1 = new float[] {0.1f};
+        when(mIPowerMock.getCpuHeadroom(eq(halParams1))).thenReturn(headroom1);
+        float[] headroom2 = new float[] {0.1f, 0.5f};
+        when(mIPowerMock.getCpuHeadroom(eq(halParams2))).thenReturn(headroom2);
+
+        HintManagerService service = createService();
+        clearInvocations(mIPowerMock);
+
+        service.getBinderServiceInstance().getCpuHeadroomMinIntervalMillis();
+        verify(mIPowerMock, times(0)).getCpuHeadroomMinIntervalMillis();
+        service.getBinderServiceInstance().getCpuHeadroom(params1);
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1));
+        service.getBinderServiceInstance().getCpuHeadroom(params2);
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams2));
+
+        // verify cache is working
+        clearInvocations(mIPowerMock);
+        assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1),
+                0.01f);
+        assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getCpuHeadroom(any());
+
+        // after 1 more second it should be served with cache still
+        Thread.sleep(1000);
+        clearInvocations(mIPowerMock);
+        assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1),
+                0.01f);
+        assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getCpuHeadroom(any());
+
+        // after 1.5 more second it should be served with cache still as timer reset
+        Thread.sleep(1500);
+        clearInvocations(mIPowerMock);
+        assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1),
+                0.01f);
+        assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getCpuHeadroom(any());
+
+        // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval
+        Thread.sleep(2100);
+        clearInvocations(mIPowerMock);
+        assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1),
+                0.01f);
+        assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1));
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams2));
+    }
+
+    @Test
+    public void testGpuHeadroomCache() throws Exception {
+        when(mIPowerMock.getGpuHeadroomMinIntervalMillis()).thenReturn(2000L);
+        GpuHeadroomParamsInternal params1 = new GpuHeadroomParamsInternal();
+        GpuHeadroomParams halParams1 = new GpuHeadroomParams();
+        halParams1.calculationType = GpuHeadroomParams.CalculationType.MIN;
+
+        GpuHeadroomParamsInternal params2 = new GpuHeadroomParamsInternal();
+        GpuHeadroomParams halParams2 = new GpuHeadroomParams();
+        params2.calculationType =
+                halParams2.calculationType = GpuHeadroomParams.CalculationType.AVERAGE;
+
+        float headroom1 = 0.1f;
+        when(mIPowerMock.getGpuHeadroom(eq(halParams1))).thenReturn(headroom1);
+        float headroom2 = 0.2f;
+        when(mIPowerMock.getGpuHeadroom(eq(halParams2))).thenReturn(headroom2);
+        HintManagerService service = createService();
+        clearInvocations(mIPowerMock);
+
+        service.getBinderServiceInstance().getGpuHeadroomMinIntervalMillis();
+        verify(mIPowerMock, times(0)).getGpuHeadroomMinIntervalMillis();
+        assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1),
+                0.01f);
+        assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams1));
+        verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2));
+
+        // verify cache is working
+        clearInvocations(mIPowerMock);
+        assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1),
+                0.01f);
+        assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getGpuHeadroom(any());
+
+        // after 1 more second it should be served with cache still
+        Thread.sleep(1000);
+        clearInvocations(mIPowerMock);
+        assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1),
+                0.01f);
+        assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getGpuHeadroom(any());
+
+        // after 1.5 more second it should be served with cache still as timer reset
+        Thread.sleep(1500);
+        clearInvocations(mIPowerMock);
+        assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1),
+                0.01f);
+        assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(0)).getGpuHeadroom(any());
+
+        // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval
+        Thread.sleep(2100);
+        clearInvocations(mIPowerMock);
+        assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1),
+                0.01f);
+        assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2),
+                0.01f);
+        verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams1));
+        verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2));
+    }
 }
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index 359cf63..b48c2d7 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -2705,12 +2705,11 @@
         verify(mInvalidateInteractiveCachesMock).call();
 
         listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
-        verify(mInvalidateInteractiveCachesMock, times(2)).call();
 
         mService.setWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP, WAKEFULNESS_ASLEEP,
                 mClock.now(), 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null);
 
-        verify(mInvalidateInteractiveCachesMock, times(3)).call();
+        verify(mInvalidateInteractiveCachesMock, times(2)).call();
     }
 
     @Test
@@ -2732,12 +2731,11 @@
         verify(mInvalidateInteractiveCachesMock).call();
 
         listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
-        verify(mInvalidateInteractiveCachesMock, times(2)).call();
 
         mService.setWakefulnessLocked(nonDefaultDisplayGroupId, WAKEFULNESS_ASLEEP, mClock.now(),
                 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null);
 
-        verify(mInvalidateInteractiveCachesMock, times(3)).call();
+        verify(mInvalidateInteractiveCachesMock, times(2)).call();
     }
 
     @Test
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
index 40e0034..0da6db6 100644
--- 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
@@ -16,9 +16,13 @@
 
 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;
@@ -29,7 +33,9 @@
 import android.annotation.SuppressLint;
 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;
@@ -41,6 +47,7 @@
 import com.android.server.ServiceThread;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
@@ -50,34 +57,36 @@
 
 public class ForensicServiceTest {
     private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
-    private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
-    private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+    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_BACKUP_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+    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 BackupTransportConnection mBackupTransportConnection;
+    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();
@@ -87,217 +96,101 @@
     }
 
     @Test
-    public void testMonitorState_Invisible() throws RemoteException {
+    public void testAddStateCallback_NoPermission() {
+        mPermissionEnforcer.revoke(READ_FORENSIC_STATE);
         StateCallback scb = new StateCallback();
         assertEquals(STATE_UNKNOWN, scb.mState);
-        mForensicService.getBinderService().monitorState(scb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb.mState);
+        assertThrows(SecurityException.class,
+                () -> mForensicService.getBinderService().addStateCallback(scb));
     }
 
     @Test
-    public void testMonitorState_Invisible_TwoMonitors() throws RemoteException {
+    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().monitorState(scb1);
+        mForensicService.getBinderService().addStateCallback(scb1);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
 
         StateCallback scb2 = new StateCallback();
         assertEquals(STATE_UNKNOWN, scb2.mState);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
     }
 
     @Test
-    public void testMakeVisible_FromInvisible() throws RemoteException {
-        StateCallback scb = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb.mState);
-        mForensicService.getBinderService().monitorState(scb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
+    public void testRemoveStateCallback() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
         doReturn(true).when(mDataAggregator).initialize();
+        doReturn(true).when(mForensicEventTransportConnection).initialize();
 
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromInvisible_TwoMonitors_DataSourceUnavailable()
-            throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-
-        doReturn(false).when(mDataAggregator).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_DATA_SOURCE_UNAVAILABLE, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromEnabled_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testMakeInvisible_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeInvisible_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeInvisible_FromEnabled_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-
-    @Test
-    public void testEnable_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        mForensicService.getBinderService().removeStateCallback(scb2);
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().enable(ccb);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
     }
 
     @Test
-    public void testEnable_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
+    public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
-        doReturn(true).when(mBackupTransportConnection).initialize();
+        doReturn(true).when(mForensicEventTransportConnection).initialize();
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().enable(ccb);
@@ -310,35 +203,13 @@
     }
 
     @Test
-    public void testEnable_FromVisible_TwoMonitors_BackupTransportUnavailable()
+    public void testEnable_FromEnabled_TwoStateCallbacks()
             throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        doReturn(false).when(mBackupTransportConnection).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_BACKUP_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testEnable_FromEnabled_TwoMonitors() throws RemoteException {
         mForensicService.setState(STATE_ENABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
         assertEquals(STATE_ENABLED, scb1.mState);
         assertEquals(STATE_ENABLED, scb2.mState);
@@ -346,62 +217,44 @@
         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_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
+    public void testDisable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().disable(ccb);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
-    }
 
-    @Test
-    public void testDisable_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().disable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
         assertNull(ccb.mErrorCode);
     }
 
     @Test
-    public void testDisable_FromEnabled_TwoMonitors() throws RemoteException {
+    public void testDisable_FromEnabled_TwoStateCallbacks() throws RemoteException {
         mForensicService.setState(STATE_ENABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
         assertEquals(STATE_ENABLED, scb1.mState);
         assertEquals(STATE_ENABLED, scb2.mState);
 
-        doNothing().when(mBackupTransportConnection).release();
+        doNothing().when(mForensicEventTransportConnection).release();
 
         ServiceThread mockThread = spy(ServiceThread.class);
         mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
@@ -412,11 +265,35 @@
         mTestLooperOfDataAggregator.dispatchAll();
         // TODO: We can verify the data sources once we implement them.
         verify(mockThread, times(1)).quitSafely();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        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);
@@ -441,14 +318,14 @@
         events.add(eventOne);
         events.add(eventTwo);
 
-        doReturn(true).when(mBackupTransportConnection).addData(any());
+        doReturn(true).when(mForensicEventTransportConnection).addData(any());
 
         mDataAggregator.addBatchData(events);
         mTestLooperOfDataAggregator.dispatchAll();
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class);
-        verify(mBackupTransportConnection).addData(captor.capture());
+        verify(mForensicEventTransportConnection).addData(captor.capture());
         List<ForensicEvent> receivedEvents = captor.getValue();
         assertEquals(receivedEvents.size(), 2);
 
@@ -476,6 +353,10 @@
             return mContext;
         }
 
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return mPermissionEnforcer;
+        }
 
         @Override
         public Looper getLooper() {
@@ -483,9 +364,9 @@
         }
 
         @Override
-        public BackupTransportConnection getBackupTransportConnection() {
-            mBackupTransportConnection = spy(new BackupTransportConnection(mContext));
-            return mBackupTransportConnection;
+        public ForensicEventTransportConnection getForensicEventransportConnection() {
+            mForensicEventTransportConnection = spy(new ForensicEventTransportConnection(mContext));
+            return mForensicEventTransportConnection;
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 698bda3..4c381eb 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -24,6 +24,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.admin.DevicePolicyManagerLiteInternal;
 import android.app.backup.IBackupManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
 import android.content.Intent;
@@ -488,6 +489,11 @@
         public Context createContextAsUser(UserHandle user) {
             return context;
         }
+
+        @Override
+        SupervisionManagerInternal getSupervisionManager() {
+            return services.supervisionManagerInternal;
+        }
     }
 
     static class TransferOwnershipMetadataManagerMockInjector extends
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index cb4269a..cf5dc4b 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -109,6 +109,7 @@
 import android.app.admin.PreferentialNetworkServiceConfig;
 import android.app.admin.SystemUpdatePolicy;
 import android.app.admin.WifiSsidPolicy;
+import android.app.admin.flags.Flags;
 import android.app.role.RoleManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -134,6 +135,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.security.KeyChain;
@@ -165,6 +170,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mockito;
 import org.mockito.internal.util.collections.Sets;
@@ -207,6 +213,9 @@
     public static final String INVALID_CALLING_IDENTITY_MSG = "Calling identity is not authorized";
     public static final String ONGOING_CALL_MSG = "ongoing call on the device";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     // TODO replace all instances of this with explicit {@link #mServiceContext}.
     @Deprecated
     private DpmMockContext mContext;
@@ -4425,6 +4434,7 @@
     }
 
     @Test
+    @Ignore("b/359188869")
     public void testSetAutoTimeEnabledWithPOOfOrganizationOwnedDevice() throws Exception {
         setupProfileOwner();
         configureProfileOwnerOfOrgOwnedDevice(admin1, CALLER_USER_HANDLE);
@@ -4902,6 +4912,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_profileOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4930,6 +4941,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_deviceOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
 
@@ -4948,6 +4960,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_nonOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4964,6 +4977,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_nonSupervisionApp() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4996,6 +5010,51 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
+    public void testIsSecondaryLockscreenEnabled() throws Exception {
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+        verifyIsSecondaryLockscreenEnabled(false);
+        verifyIsSecondaryLockscreenEnabled(true);
+    }
+
+    private void verifyIsSecondaryLockscreenEnabled(boolean expected) throws Exception {
+        reset(getServices().supervisionManagerInternal);
+
+        doReturn(expected).when(getServices().supervisionManagerInternal)
+                .isSupervisionLockscreenEnabledForUser(anyInt());
+
+        final boolean enabled = dpm.isSecondaryLockscreenEnabled(UserHandle.of(CALLER_USER_HANDLE));
+        verify(getServices().supervisionManagerInternal)
+                .isSupervisionLockscreenEnabledForUser(CALLER_USER_HANDLE);
+
+        assertThat(enabled).isEqualTo(expected);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
+    public void testSetSecondaryLockscreenEnabled() throws Exception {
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+        verifySetSecondaryLockscreenEnabled(false);
+        verifySetSecondaryLockscreenEnabled(true);
+    }
+
+    private void verifySetSecondaryLockscreenEnabled(boolean enabled) throws Exception {
+        reset(getServices().supervisionManagerInternal);
+
+        dpm.setSecondaryLockscreenEnabled(admin1, enabled);
+        verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser(
+                CALLER_USER_HANDLE, enabled, null);
+
+        reset(getServices().supervisionManagerInternal);
+
+        dpm.setSecondaryLockscreenEnabled(enabled, new PersistableBundle());
+        verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser(
+                eq(CALLER_USER_HANDLE), eq(enabled), any(PersistableBundle.class));
+    }
+
+    @Test
     public void testIsDeviceManaged() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
         setupDeviceOwner();
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
index 2e200a9..3e4448c1 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
@@ -35,6 +35,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.backup.IBackupManager;
 import android.app.role.RoleManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -77,8 +78,8 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.server.AlarmManagerInternal;
-import com.android.server.pdb.PersistentDataBlockManagerInternal;
 import com.android.server.net.NetworkPolicyManagerInternal;
+import com.android.server.pdb.PersistentDataBlockManagerInternal;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.pkg.PackageState;
@@ -149,6 +150,7 @@
     public final BuildMock buildMock = new BuildMock();
     public final File dataDir;
     public final PolicyPathProvider pathProvider;
+    public final SupervisionManagerInternal supervisionManagerInternal;
 
     private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>();
 
@@ -203,6 +205,7 @@
         roleManager = realContext.getSystemService(RoleManager.class);
         roleManagerForMock = mock(RoleManagerForMock.class);
         subscriptionManager = mock(SubscriptionManager.class);
+        supervisionManagerInternal = mock(SupervisionManagerInternal.class);
 
         // Package manager is huge, so we use a partial mock instead.
         packageManager = spy(realContext.getPackageManager());
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 73aec63..510c2bc 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -532,6 +532,8 @@
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
         projection.start(mIMediaProjectionCallback);
 
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, projection.packageName);
         doReturn(true).when(mKeyguardManager).isKeyguardLocked();
         MediaProjectionManagerService.BinderService mediaProjectionBinderService =
                 mService.new BinderService(mContext);
@@ -540,50 +542,6 @@
         verify(mContext, never()).startActivityAsUser(any(), any());
     }
 
-    @EnableFlags(android.companion.virtualdevice.flags
-            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
-    @Test
-    public void testKeyguardLocked_stopsActiveProjection() throws Exception {
-        MediaProjectionManagerService service =
-                new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
-        MediaProjectionManagerService.MediaProjection projection =
-                startProjectionPreconditions(service);
-        projection.start(mIMediaProjectionCallback);
-        projection.notifyVirtualDisplayCreated(10);
-
-        assertThat(service.getActiveProjectionInfo()).isNotNull();
-
-        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager)
-                .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName);
-        service.onKeyguardLockedStateChanged(true);
-
-        verify(mMediaProjectionMetricsLogger).logStopped(UID, TARGET_UID_UNKNOWN);
-        assertThat(service.getActiveProjectionInfo()).isNull();
-        assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue();
-    }
-
-    @EnableFlags(android.companion.virtualdevice.flags
-            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
-    @Test
-    public void testKeyguardLocked_packageAllowlisted_doesNotStopActiveProjection()
-            throws NameNotFoundException {
-        MediaProjectionManagerService service =
-                new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
-        MediaProjectionManagerService.MediaProjection projection =
-                startProjectionPreconditions(service);
-        projection.start(mIMediaProjectionCallback);
-        projection.notifyVirtualDisplayCreated(10);
-
-        assertThat(service.getActiveProjectionInfo()).isNotNull();
-
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission(
-                RECORD_SENSITIVE_CONTENT, projection.packageName);
-        service.onKeyguardLockedStateChanged(true);
-
-        verifyZeroInteractions(mMediaProjectionMetricsLogger);
-        assertThat(service.getActiveProjectionInfo()).isNotNull();
-    }
-
     @Test
     public void stop_noActiveProjections_doesNotLog() throws Exception {
         MediaProjectionManagerService service =
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java
new file mode 100644
index 0000000..89d2d28
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java
@@ -0,0 +1,429 @@
+/*
+ * 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.media.projection;
+
+
+import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
+import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.app.role.RoleManager;
+import android.companion.AssociationRequest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.projection.MediaProjectionManager;
+import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.testing.TestableContext;
+import android.util.ArraySet;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemConfig;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Tests for the {@link MediaProjectionStopController} class.
+ * <p>
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:MediaProjectionStopControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SuppressLint({"UseCheckPermission", "VisibleForTests", "MissingPermission"})
+public class MediaProjectionStopControllerTest {
+    private static final int UID = 10;
+    private static final String PACKAGE_NAME = "test.package";
+    private final ApplicationInfo mAppInfo = new ApplicationInfo();
+    @Rule
+    public final TestableContext mContext = spy(
+            new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()));
+
+    private final MediaProjectionManagerService.Injector mMediaProjectionMetricsLoggerInjector =
+            new MediaProjectionManagerService.Injector() {
+                @Override
+                MediaProjectionMetricsLogger mediaProjectionMetricsLogger(Context context) {
+                    return mMediaProjectionMetricsLogger;
+                }
+            };
+
+    private MediaProjectionManagerService mService;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock
+    private ActivityManagerInternal mAmInternal;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private KeyguardManager mKeyguardManager;
+    @Mock
+    private TelecomManager mTelecomManager;
+
+    private AppOpsManager mAppOpsManager;
+    @Mock
+    private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
+    @Mock
+    private Consumer<Integer> mStopConsumer;
+
+    private MediaProjectionStopController mStopController;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.addService(ActivityManagerInternal.class, mAmInternal);
+
+        mAppOpsManager = mockAppOpsManager();
+        mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
+        mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
+        mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
+        mContext.setMockPackageManager(mPackageManager);
+
+        mStopController = new MediaProjectionStopController(mContext, mStopConsumer);
+        mService = new MediaProjectionManagerService(mContext,
+                mMediaProjectionMetricsLoggerInjector);
+
+        mAppInfo.targetSdkVersion = 35;
+    }
+
+    private static AppOpsManager mockAppOpsManager() {
+        return mock(AppOpsManager.class, invocationOnMock -> {
+            if (invocationOnMock.getMethod().getName().startsWith("noteOp")) {
+                // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED
+                // and is not what we want.
+                return AppOpsManager.MODE_IGNORED;
+            }
+            return Answers.RETURNS_DEFAULTS.answer(invocationOnMock);
+        });
+    }
+
+    @After
+    public void tearDown() {
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
+    }
+
+    @Test
+    @EnableFlags(
+            android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+    public void testMediaProjectionNotRestricted() throws Exception {
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+
+        assertThat(mStopController.isStartForbidden(
+                createMediaProjection(PACKAGE_NAME))).isFalse();
+    }
+
+    @Test
+    @EnableFlags(
+            android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+    public void testMediaProjectionRestricted() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+        mediaProjection.notifyVirtualDisplayCreated(1);
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+        assertThat(mStopController.isStartForbidden(mediaProjection)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingNullProjection() throws Exception {
+        assertThat(mStopController.isExemptFromStopping(null)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingInvalidProjection() throws Exception {
+        assertThat(mStopController.isExemptFromStopping(createMediaProjection(null))).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingDisableScreenshareProtections() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        int value = Settings.Global.getInt(mContext.getContentResolver(),
+                DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0);
+        try {
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 1);
+
+            assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+        } finally {
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, value);
+        }
+    }
+
+    @Test
+    public void testExemptFromStoppingHasOpProjectMedia() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager)
+                .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA),
+                        eq(mediaProjection.uid), eq(mediaProjection.packageName),
+                        nullable(String.class),
+                        nullable(String.class));
+        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingHasAppStreamingRole() throws Exception {
+        runWithRole(
+                AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+                () -> {
+                    try {
+                        MediaProjectionManagerService.MediaProjection mediaProjection =
+                                createMediaProjection();
+                        doReturn(PackageManager.PERMISSION_DENIED).when(
+                                mPackageManager).checkPermission(
+                                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+                        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    @Test
+    public void testExemptFromStoppingIsBugreportAllowlisted() throws Exception {
+        ArraySet<String> packages = SystemConfig.getInstance().getBugreportWhitelistedPackages();
+        if (packages.isEmpty()) {
+            return;
+        }
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection(
+                packages.valueAt(0));
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingHasNoDisplay() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection(
+                PACKAGE_NAME);
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingHasRecordSensitiveContentPermission() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isTrue();
+    }
+
+    @Test
+    public void testExemptFromStoppingIsFalse() throws Exception {
+        MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+        mediaProjection.notifyVirtualDisplayCreated(1);
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+        assertThat(mStopController.isExemptFromStopping(mediaProjection)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(
+            android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+    public void testKeyguardLockedStateChanged_unlocked() {
+        mStopController.onKeyguardLockedStateChanged(false);
+
+        verify(mStopConsumer, never()).accept(anyInt());
+    }
+
+    @Test
+    @EnableFlags(
+            android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+    public void testKeyguardLockedStateChanged_locked() {
+        mStopController.onKeyguardLockedStateChanged(true);
+
+        verify(mStopConsumer).accept(MediaProjectionStopController.STOP_REASON_KEYGUARD);
+    }
+
+    @Test
+    @EnableFlags(com.android.media.projection.flags.Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    public void testCallStateChanged_callStarts() {
+        // Setup call state to false
+        when(mTelecomManager.isInCall()).thenReturn(false);
+        mStopController.callStateChanged();
+
+        clearInvocations(mStopConsumer);
+
+        when(mTelecomManager.isInCall()).thenReturn(true);
+        mStopController.callStateChanged();
+
+        verify(mStopConsumer, never()).accept(anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.media.projection.flags.Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    public void testCallStateChanged_remainsInCall() {
+        // Setup call state to false
+        when(mTelecomManager.isInCall()).thenReturn(true);
+        mStopController.callStateChanged();
+
+        clearInvocations(mStopConsumer);
+
+        when(mTelecomManager.isInCall()).thenReturn(true);
+        mStopController.callStateChanged();
+
+        verify(mStopConsumer, never()).accept(anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.media.projection.flags.Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    public void testCallStateChanged_remainsNoCall() {
+        // Setup call state to false
+        when(mTelecomManager.isInCall()).thenReturn(false);
+        mStopController.callStateChanged();
+
+        clearInvocations(mStopConsumer);
+
+        when(mTelecomManager.isInCall()).thenReturn(false);
+        mStopController.callStateChanged();
+
+        verify(mStopConsumer, never()).accept(anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.media.projection.flags.Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    public void testCallStateChanged_callEnds() {
+        // Setup call state to false
+        when(mTelecomManager.isInCall()).thenReturn(true);
+        mStopController.callStateChanged();
+
+        clearInvocations(mStopConsumer);
+
+        when(mTelecomManager.isInCall()).thenReturn(false);
+        mStopController.callStateChanged();
+
+        verify(mStopConsumer).accept(MediaProjectionStopController.STOP_REASON_CALL_END);
+    }
+
+    private MediaProjectionManagerService.MediaProjection createMediaProjection()
+            throws NameNotFoundException {
+        return createMediaProjection(PACKAGE_NAME);
+    }
+
+    private MediaProjectionManagerService.MediaProjection createMediaProjection(String packageName)
+            throws NameNotFoundException {
+        doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
+                any(ApplicationInfoFlags.class), any(UserHandle.class));
+        doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(Mockito.isNull(),
+                any(ApplicationInfoFlags.class), any(UserHandle.class));
+        return mService.createProjectionInternal(UID, packageName,
+                MediaProjectionManager.TYPE_SCREEN_CAPTURE, false, mContext.getUser(),
+                INVALID_DISPLAY);
+    }
+
+    /**
+     * Run the provided block giving the current context's package the provided role.
+     */
+    @SuppressWarnings("SameParameterValue")
+    private void runWithRole(String role, Runnable block) {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        String packageName = mContext.getPackageName();
+        UserHandle user = instrumentation.getTargetContext().getUser();
+        RoleManager roleManager = Objects.requireNonNull(
+                mContext.getSystemService(RoleManager.class));
+        try {
+            CountDownLatch latch = new CountDownLatch(1);
+            instrumentation.getUiAutomation().adoptShellPermissionIdentity(
+                    Manifest.permission.MANAGE_ROLE_HOLDERS,
+                    Manifest.permission.BYPASS_ROLE_QUALIFICATION);
+
+            roleManager.setBypassingRoleQualification(true);
+            roleManager.addRoleHolderAsUser(role, packageName,
+                    /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
+                    mContext.getMainExecutor(), success -> {
+                        if (success) {
+                            latch.countDown();
+                        } else {
+                            Assert.fail("Couldn't set role for test (failure) " + role);
+                        }
+                    });
+            assertWithMessage("Couldn't set role for test (timeout) : " + role)
+                    .that(latch.await(1, TimeUnit.SECONDS)).isTrue();
+            block.run();
+
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } finally {
+            roleManager.removeRoleHolderAsUser(role, packageName,
+                    /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
+                    mContext.getMainExecutor(), (aBool) -> {
+                    });
+            roleManager.setBypassingRoleQualification(false);
+            instrumentation.getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index 79b06236..8290e1c 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -20,7 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.UserInfo
-import android.os.Bundle
+import android.os.PersistableBundle
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -139,7 +139,7 @@
         assertThat(userData.supervisionLockScreenEnabled).isFalse()
         assertThat(userData.supervisionLockScreenOptions).isNull()
 
-        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, PersistableBundle())
         userData = service.getUserDataLocked(USER_ID)
         assertThat(userData.supervisionLockScreenEnabled).isTrue()
         assertThat(userData.supervisionLockScreenOptions).isNotNull()
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index cc02865..6af6542 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -84,7 +84,9 @@
 import com.android.internal.R;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.GroupHelper.CachedSummary;
+import com.android.server.notification.GroupHelper.FullyQualifiedGroupKey;
 import com.android.server.notification.GroupHelper.NotificationAttributes;
+import com.android.server.notification.GroupHelper.NotificationSectioner;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -2298,6 +2300,7 @@
         final String pkg = "package";
         final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
                 AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final int numNotifications = 2 * AUTOGROUP_AT_COUNT;
         int numNotificationChannel1 = 0;
         final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1",
                 "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT);
@@ -2307,7 +2310,7 @@
         final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
         // Post notifications with different channels that autogroup within the same section
         NotificationRecord r;
-        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+        for (int i = 0; i < numNotifications; i++) {
             if (i % 2 == 0) {
                 r = getNotificationRecord(pkg, i, String.valueOf(i),
                         UserHandle.SYSTEM, "testGrp " + i, false, channel1);
@@ -2324,12 +2327,12 @@
                 "TEST_CHANNEL_ID1");
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(expectedGroupKey_alerting), anyInt(), eq(expectedSummaryAttr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+        verify(mCallback, times(numNotifications)).addAutoGroup(anyString(),
                 eq(expectedGroupKey_alerting), eq(true));
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
-                any());
+        verify(mCallback, times(numNotifications - AUTOGROUP_AT_COUNT)).updateAutogroupSummary(
+                anyInt(), anyString(), anyString(), any());
         Mockito.reset(mCallback);
 
         // Update channel1's importance
@@ -2375,7 +2378,7 @@
         final List<NotificationRecord> notificationList = new ArrayList<>();
         final String pkg = "package";
         final int summaryId = 0;
-        final int numChildNotif = 4;
+        final int numChildNotif = 2 * AUTOGROUP_AT_COUNT;
 
         // Create an app-provided group: summary + child notifications
         final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1",
@@ -2435,8 +2438,8 @@
                 eq(expectedGroupKey_social), eq(true));
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
-        verify(mCallback, times(numChildNotif / 2)).updateAutogroupSummary(anyInt(), anyString(),
-                anyString(), any());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
         verify(mCallback, times(numChildNotif)).removeAppProvidedSummaryOnClassification(
                 anyString(), eq(originalAppGroupKey));
     }
@@ -2513,9 +2516,10 @@
         final List<NotificationRecord> notificationList = new ArrayList<>();
         final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
         final String pkg = "package";
+        final int numChildNotifications = AUTOGROUP_AT_COUNT;
 
         // Post singleton groups, above forced group limit
-        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+        for (int i = 0; i < numChildNotifications; i++) {
             NotificationRecord summary = getNotificationRecord(pkg, i,
                     String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true);
             notificationList.add(summary);
@@ -2545,13 +2549,13 @@
         // Check that notifications are forced grouped and app-provided summaries are canceled
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
-        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(),
+        verify(mCallback, times(numChildNotifications)).addAutoGroup(anyString(),
                 eq(expectedGroupKey_social), eq(true));
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 any());
-        verify(mCallback, times(2)).removeAppProvidedSummaryOnClassification(
+        verify(mCallback, times(numChildNotifications)).removeAppProvidedSummaryOnClassification(
                 anyString(), anyString());
 
         // Adjust group key and cancel summaries
@@ -2593,14 +2597,16 @@
                 AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
         String expectedTriggeringKey = null;
         // Post singleton groups, above forced group limit
-        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             NotificationRecord summary = getNotificationRecord(pkg, i,
                     String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true);
             notificationList.add(summary);
             NotificationRecord child = getNotificationRecord(pkg, i + 42,
                     String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp " + i, false);
             notificationList.add(child);
-            expectedTriggeringKey = child.getKey();
+            if (i == AUTOGROUP_SINGLETONS_AT_COUNT - 1) {
+                expectedTriggeringKey = child.getKey();
+            }
             summaryByGroup.put(summary.getGroupKey(), summary);
             mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
             summary.isCanceled = true;  // simulate removing the app summary
@@ -2611,14 +2617,8 @@
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg),
                 eq(expectedTriggeringKey), eq(expectedGroupKey_alerting), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(),
-                eq(expectedGroupKey_alerting), eq(true));
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
-                any());
-        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).removeAppProvidedSummary(
-                anyString());
         assertThat(mGroupHelper.findCanceledSummary(pkg, String.valueOf(0), 0,
                 UserHandle.SYSTEM.getIdentifier())).isNotNull();
         assertThat(mGroupHelper.findCanceledSummary(pkg, String.valueOf(1), 1,
@@ -2645,12 +2645,12 @@
         // Check that all notifications are moved to the social section group
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
-        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(),
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
                 eq(expectedGroupKey_social), eq(true));
         // Check that the alerting section group is removed
         verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
                 eq(expectedGroupKey_alerting));
-        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).updateAutogroupSummary(anyInt(),
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).updateAutogroupSummary(anyInt(),
                 anyString(), anyString(), any());
     }
 
@@ -2666,7 +2666,7 @@
         final String pkg = "package";
 
         final int summaryId = 0;
-        final int numChildren = 3;
+        final int numChildren = AUTOGROUP_AT_COUNT;
         // Post a regular/valid group: summary + notifications
         NotificationRecord summary = getNotificationRecord(pkg, summaryId,
                 String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
@@ -2706,13 +2706,211 @@
                 eq(true));
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
-        verify(mCallback, times(numChildren - 1)).updateAutogroupSummary(anyInt(), anyString(),
-                anyString(), any());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
         verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification(anyString(),
                 anyString());
     }
 
     @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
+            FLAG_NOTIFICATION_CLASSIFICATION})
+    public void testUnbundleNotification_originalSummaryMissing_autogroupInNewSection() {
+        // Check that unbundled notifications are moved to the original section and aggregated
+        // with existing autogrouped notifications
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+
+        final int summaryId = 0;
+        final int numChildren = AUTOGROUP_AT_COUNT - 1;
+        // Post a regular/valid group: summary + notifications (one less than autogroup limit)
+        NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+                String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(summary);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        final String originalAppGroupKey = summary.getGroupKey();
+        final NotificationChannel originalChannel = summary.getChannel();
+        for (int i = 0; i < numChildren; i++) {
+            NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+                    UserHandle.SYSTEM, "testGrp", false);
+            notificationList.add(child);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+        }
+
+        // Classify/bundle all child notifications: original group & summary is removed
+        final NotificationChannel socialChannel = new NotificationChannel(
+                NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+                IMPORTANCE_DEFAULT);
+        for (NotificationRecord record: notificationList) {
+            if (record.getOriginalGroupKey().contains("testGrp")
+                    && record.getNotification().isGroupChild()) {
+                record.updateNotificationChannel(socialChannel);
+                mGroupHelper.onChannelUpdated(record);
+            }
+        }
+
+        // Check that no autogroup summaries were created for the social section
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification(
+                anyString(), eq(originalAppGroupKey));
+
+        // Cancel summary
+        summary.isCanceled = true;
+        summaryByGroup.clear();
+        notificationList.remove(summary);
+
+        // Add 1 ungrouped notification in the original section
+        NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242,
+                String.valueOf(4242), UserHandle.SYSTEM);
+        notificationList.add(ungroupedNotification);
+        mGroupHelper.onNotificationPosted(ungroupedNotification, false);
+
+        // Unbundle the bundled notifications => notifications are moved back to the original group
+        // and an aggregate group is created because autogroup limit is reached
+        reset(mCallback);
+        for (NotificationRecord record: notificationList) {
+            if (record.getNotification().isGroupChild()
+                    && record.getOriginalGroupKey().contains("testGrp")
+                    && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+                        record.getChannel().getId())) {
+                record.updateNotificationChannel(originalChannel);
+                mGroupHelper.onNotificationUnbundled(record, false);
+            }
+        }
+
+        // Check that a new aggregate group is created
+        final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_alerting), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_alerting), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, times(numChildren)).removeAutoGroupSummary(anyInt(), anyString(),
+                anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
+            FLAG_NOTIFICATION_CLASSIFICATION})
+    public void testUnbundleNotification_originalSummaryExists() {
+        // Check that unbundled notifications are moved to the original section and original group
+        // when the original summary is still present
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+
+        final int summaryId = 0;
+        final int numChildren = AUTOGROUP_AT_COUNT + 1;
+        // Post a regular/valid group: summary + notifications
+        NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+                String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(summary);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        final String originalAppGroupKey = summary.getGroupKey();
+        final NotificationChannel originalChannel = summary.getChannel();
+        for (int i = 0; i < numChildren; i++) {
+            NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+                    UserHandle.SYSTEM, "testGrp", false);
+            notificationList.add(child);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+        }
+
+        // Classify/bundle child notifications: all except one, to keep the original group
+        final NotificationChannel socialChannel = new NotificationChannel(
+                NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+                IMPORTANCE_DEFAULT);
+        final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
+        final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
+                BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                NotificationChannel.SOCIAL_MEDIA_ID);
+        int numChildrenBundled = 0;
+        for (NotificationRecord record: notificationList) {
+            if (record.getOriginalGroupKey().contains("testGrp")
+                    && record.getNotification().isGroupChild()) {
+                record.updateNotificationChannel(socialChannel);
+                mGroupHelper.onChannelUpdated(record);
+                numChildrenBundled++;
+                if (numChildrenBundled == AUTOGROUP_AT_COUNT) {
+                    break;
+                }
+            }
+        }
+
+        // Check that 1 autogroup summaries were created for the social section
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_social), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification(
+                anyString(), eq(originalAppGroupKey));
+
+        // Adjust group key and cancel summaries
+        for (NotificationRecord record: notificationList) {
+            if (record.getNotification().isGroupSummary()) {
+                record.isCanceled = true;
+            } else if (record.getOriginalGroupKey().contains("testGrp")
+                        && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+                        record.getChannel().getId())) {
+                record.setOverrideGroupKey(expectedGroupKey_social);
+            }
+        }
+
+        // Add 1 ungrouped notification in the original section
+        NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242,
+                String.valueOf(4242), UserHandle.SYSTEM);
+        notificationList.add(ungroupedNotification);
+        mGroupHelper.onNotificationPosted(ungroupedNotification, false);
+
+        // Unbundle the bundled notifications => social section summary is destroyed
+        // and notifications are moved back to the original group
+        reset(mCallback);
+        for (NotificationRecord record: notificationList) {
+            if (record.getNotification().isGroupChild()
+                    && record.getOriginalGroupKey().contains("testGrp")
+                    && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+                        record.getChannel().getId())) {
+                record.updateNotificationChannel(originalChannel);
+                mGroupHelper.onNotificationUnbundled(record, true);
+            }
+        }
+
+        // Check that the autogroup summary for the social section was removed
+        // and that no new autogroup summaries were created
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_social));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).updateAutogroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_social), any());
+
+        for (NotificationRecord record: notificationList) {
+            if (record.getNotification().isGroupChild()
+                    && record.getOriginalGroupKey().contains("testGrp")) {
+                assertThat(record.getSbn().getOverrideGroupKey()).isNull();
+            }
+        }
+    }
+
+    @Test
     @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testMoveAggregateGroups_updateChannel_groupsUngrouped() {
         final String pkg = "package";
@@ -3059,6 +3257,120 @@
     @Test
     @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS)
+    public void testNonGroupableChildren_singletonGroups_disableConversations() {
+        // Check that singleton groups with children that are not groupable, is not grouped
+        // Even though the group summary is a regular (alerting) notification, the children are
+        // conversations => the group should not be forced grouped.
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+
+        // Trigger notification, ungrouped
+        final int triggerId = 1;
+        NotificationRecord triggerNotification = getNotificationRecord(pkg, triggerId,
+                String.valueOf(triggerId), UserHandle.SYSTEM);
+        notificationList.add(triggerNotification);
+        final NotificationSectioner triggerSection = GroupHelper.getSection(triggerNotification);
+        final FullyQualifiedGroupKey triggerFullAggregateGroupKey = new FullyQualifiedGroupKey(
+                triggerNotification.getUserId(), triggerNotification.getSbn().getPackageName(),
+                triggerSection);
+
+        // Add singleton group with alerting child
+        final String groupName_valid = "testGrp_valid";
+        final int summaryId_valid = 0;
+        NotificationRecord summary = getNotificationRecord(pkg, summaryId_valid,
+                String.valueOf(summaryId_valid), UserHandle.SYSTEM, groupName_valid, true);
+        notificationList.add(summary);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        final String groupKey_valid = summary.getGroupKey();
+        NotificationRecord child = getNotificationRecord(pkg, summaryId_valid + 42,
+                String.valueOf(summaryId_valid + 42), UserHandle.SYSTEM, groupName_valid, false);
+        notificationList.add(child);
+
+        // Add singleton group with conversation child
+        final String groupName_invalid = "testGrp_invalid";
+        final int summaryId_invalid = 100;
+        summary = getNotificationRecord(pkg, summaryId_invalid,
+                String.valueOf(summaryId_invalid), UserHandle.SYSTEM, groupName_invalid, true);
+        notificationList.add(summary);
+        final String groupKey_invalid = summary.getGroupKey();
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        child = getNotificationRecord(pkg, summaryId_invalid + 42,
+                String.valueOf(summaryId_invalid + 42), UserHandle.SYSTEM, groupName_invalid,
+                false);
+        child = spy(child);
+        when(child.isConversation()).thenReturn(true);
+        notificationList.add(child);
+
+        // Check that the invalid group will not be force grouped
+        final ArrayMap<String, NotificationRecord> sparseGroups = mGroupHelper.getSparseGroups(
+                triggerFullAggregateGroupKey, notificationList, summaryByGroup, triggerSection);
+        assertThat(sparseGroups).containsKey(groupKey_valid);
+        assertThat(sparseGroups).doesNotContainKey(groupKey_invalid);
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS})
+    public void testNonGroupableChildren_singletonGroups_enableConversations() {
+        // Check that singleton groups with children that are not groupable, is not grouped
+        // Conversations are groupable (FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS is enabled)
+        // The invalid group is the alerting notifications: because the triggering notifications'
+        // section is Conversations, so the alerting group should be skipped.
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+
+        // Trigger notification, ungrouped conversation
+        final int triggerId = 1;
+        NotificationRecord triggerNotification = getNotificationRecord(pkg, triggerId,
+                String.valueOf(triggerId), UserHandle.SYSTEM);
+        triggerNotification = spy(triggerNotification);
+        when(triggerNotification.isConversation()).thenReturn(true);
+        notificationList.add(triggerNotification);
+        final NotificationSectioner triggerSection = GroupHelper.getSection(triggerNotification);
+        final FullyQualifiedGroupKey triggerFullAggregateGroupKey = new FullyQualifiedGroupKey(
+                triggerNotification.getUserId(), triggerNotification.getSbn().getPackageName(),
+                triggerSection);
+
+        // Add singleton group with conversation child
+        final String groupName_valid = "testGrp_valid";
+        final int summaryId_valid = 0;
+        NotificationRecord summary = getNotificationRecord(pkg, summaryId_valid,
+                String.valueOf(summaryId_valid), UserHandle.SYSTEM, groupName_valid, true);
+        summary = spy(summary);
+        when(summary.isConversation()).thenReturn(true);
+        notificationList.add(summary);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        final String groupKey_valid = summary.getGroupKey();
+        NotificationRecord child = getNotificationRecord(pkg, summaryId_valid + 42,
+                String.valueOf(summaryId_valid + 42), UserHandle.SYSTEM, groupName_valid, false);
+        child = spy(child);
+        when(child.isConversation()).thenReturn(true);
+        notificationList.add(child);
+
+        // Add singleton group with non-conversation child
+        final String groupName_invalid = "testGrp_invalid";
+        final int summaryId_invalid = 100;
+        summary = getNotificationRecord(pkg, summaryId_invalid,
+                String.valueOf(summaryId_invalid), UserHandle.SYSTEM, groupName_invalid, true);
+        notificationList.add(summary);
+        final String groupKey_invalid = summary.getGroupKey();
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        child = getNotificationRecord(pkg, summaryId_invalid + 42,
+                String.valueOf(summaryId_invalid + 42), UserHandle.SYSTEM, groupName_invalid,
+                false);
+        notificationList.add(child);
+
+        // Check that the invalid group will not be force grouped
+        final ArrayMap<String, NotificationRecord> sparseGroups = mGroupHelper.getSparseGroups(
+                triggerFullAggregateGroupKey, notificationList, summaryByGroup, triggerSection);
+        assertThat(sparseGroups).containsKey(groupKey_valid);
+        assertThat(sparseGroups).doesNotContainKey(groupKey_invalid);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS)
     public void testNonGroupableNotifications() {
         // Check that there is no valid section for: conversations, calls, foreground services
         NotificationRecord notification_conversation = mock(NotificationRecord.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 42e31de..817c368 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -82,6 +82,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
@@ -122,6 +123,8 @@
  * Build/Install/Run:
  *  atest WmTests:WindowOrganizerTests
  */
+
+// TODO revert parts of this set to set the flag to test the behavior
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)
@@ -1299,6 +1302,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2)
     public void testEnterPipParams() {
         final StubOrganizer o = new StubOrganizer();
         mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(o);
@@ -1314,6 +1318,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2)
     public void testChangePipParams() {
         class ChangeSavingOrganizer extends StubOrganizer {
             RunningTaskInfo mChangedInfo;
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 9f5e6d1..7e600b3 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
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.helpers
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
 import android.graphics.Insets
 import android.graphics.Rect
@@ -74,13 +75,28 @@
             .waitForAndVerify()
     }
 
+    /** Launch an app and ensure it's moved to Desktop if it has not. */
+    fun enterDesktopMode(
+        wmHelper: WindowManagerStateHelper,
+        device: UiDevice,
+        motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH),
+    ) {
+        innerHelper.launchViaIntent(wmHelper)
+        if (!isInDesktopWindowingMode(wmHelper)) {
+            enterDesktopModeWithDrag(
+                wmHelper = wmHelper,
+                device = device,
+                motionEventHelper = motionEventHelper
+            )
+        }
+    }
+
     /** Move an app to Desktop by dragging the app handle at the top. */
-    fun enterDesktopWithDrag(
+    fun enterDesktopModeWithDrag(
         wmHelper: WindowManagerStateHelper,
         device: UiDevice,
         motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH)
     ) {
-        innerHelper.launchViaIntent(wmHelper)
         dragToDesktop(
             wmHelper = wmHelper,
             device = device,
@@ -415,6 +431,10 @@
         return metricInsets.getInsetsIgnoringVisibility(typeMask)
     }
 
+    // Requirement of DesktopWindowingMode is having a minimum of 1 app in WINDOWING_MODE_FREEFORM.
+    private fun isInDesktopWindowingMode(wmHelper: WindowManagerStateHelper) =
+        wmHelper.getWindow(innerHelper)?.windowingMode == WINDOWING_MODE_FREEFORM
+
     private companion object {
         val TIMEOUT: Duration = Duration.ofSeconds(3)
         const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
index 2692e12..44641f7 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -57,6 +58,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
 
 import perfetto.protos.Protolog;
 import perfetto.protos.ProtologCommon;
@@ -858,6 +860,39 @@
                 .isEqualTo("This message should also be logged 567");
     }
 
+    @Test
+    public void enablesLogGroupAfterLoadingConfig() {
+        sProtoLog.stopLoggingToLogcat(
+                new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {});
+        Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse();
+
+        doAnswer((Answer<Void>) invocation -> {
+            // logToLogcat is still false before we laod the viewer config
+            Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse();
+            return null;
+        }).when(sReader).unloadViewerConfig(any(), any());
+
+        sProtoLog.startLoggingToLogcat(
+                new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {});
+        Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isTrue();
+    }
+
+    @Test
+    public void disablesLogGroupBeforeUnloadingConfig() {
+        sProtoLog.startLoggingToLogcat(
+                new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {});
+        Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isTrue();
+
+        doAnswer((Answer<Void>) invocation -> {
+            // Already set logToLogcat to false by the time we unload the config
+            Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse();
+            return null;
+        }).when(sReader).unloadViewerConfig(any(), any());
+        sProtoLog.stopLoggingToLogcat(
+                new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {});
+        Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse();
+    }
+
     private enum TestProtoLogGroup implements IProtoLogGroup {
         TEST_GROUP(true, true, false, "TEST_TAG");
 
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 1bef5f8..1d4adc4 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -207,14 +207,13 @@
   Value* dst_value = dst_config_value->value.get();
   Value* src_value = src_config_value->value.get();
 
-  CollisionResult collision_result;
-  if (overlay) {
-    collision_result =
-        ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool);
-  } else {
-    collision_result =
-        ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus());
-    if (collision_result == CollisionResult::kConflict) {
+  CollisionResult collision_result =
+      ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus());
+  if (collision_result == CollisionResult::kConflict) {
+    if (overlay) {
+      collision_result =
+          ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool);
+    } else {
       collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value);
     }
   }