Merge "MediaQuality: Declare flag for media quality apis" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0f3b1c3..033da2d 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4944,10 +4944,14 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
             final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
             synchronized (mLock) {
                 String pkgList[] = null;
-                switch (intent.getAction()) {
+                switch (action) {
                     case Intent.ACTION_QUERY_PACKAGE_RESTART:
                         pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
                         for (String packageName : pkgList) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 8eb8811..836d4e99 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6854,6 +6854,47 @@
     method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri);
   }
 
+  @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style {
+    ctor public Notification.ProgressStyle();
+    method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment);
+    method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step);
+    method public int getProgress();
+    method @Nullable public android.graphics.drawable.Icon getProgressEndIcon();
+    method public int getProgressMax();
+    method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments();
+    method @Nullable public android.graphics.drawable.Icon getProgressStartIcon();
+    method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps();
+    method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon();
+    method public boolean isProgressIndeterminate();
+    method public boolean isStyledByProgress();
+    method @NonNull public android.app.Notification.ProgressStyle setProgress(int);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>);
+    method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon);
+    method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean);
+  }
+
+  public static final class Notification.ProgressStyle.Segment {
+    ctor public Notification.ProgressStyle.Segment(int);
+    method @ColorInt public int getColor();
+    method public int getLength();
+    method public int getStableId();
+    method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int);
+    method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int);
+  }
+
+  public static final class Notification.ProgressStyle.Step {
+    ctor public Notification.ProgressStyle.Step(int);
+    method @ColorInt public int getColor();
+    method public int getPosition();
+    method public int getStableId();
+    method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int);
+    method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int);
+  }
+
   public abstract static class Notification.Style {
     ctor @Deprecated public Notification.Style();
     method public android.app.Notification build();
@@ -8732,13 +8773,15 @@
 package android.app.appfunctions {
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
-    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
   }
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
 
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bfddf4f..a1561c2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6833,6 +6833,16 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
   }
 
+  public static final class SoundTrigger.RecognitionConfig.Builder {
+    ctor public SoundTrigger.RecognitionConfig.Builder();
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig build();
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAllowMultipleTriggers(boolean);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAudioCapabilities(int);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setCaptureRequested(boolean);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setData(@Nullable byte[]);
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setKeyphrases(@NonNull java.util.Collection<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
+  }
+
   public static class SoundTrigger.RecognitionEvent {
     method @Nullable public android.media.AudioFormat getCaptureFormat();
     method public int getCaptureSession();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index cc9e836..0a10920 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1614,15 +1614,15 @@
   public final class CameraManager {
     method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
     method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
-    method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
+    method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
     method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String);
     field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait";
     field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
-    field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
+    field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
   }
 
   public abstract static class CameraManager.AvailabilityCallback {
@@ -1887,7 +1887,7 @@
   }
 
   @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
-    ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
+    ctor @Deprecated public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
     ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]);
   }
 
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 392a1f1..a39f216 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -121,7 +121,6 @@
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -783,10 +782,32 @@
     @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
     public static final int FLAG_PROMOTED_ONGOING = 0x00040000;
 
-    private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
-            BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
-            DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
-            MessagingStyle.class, CallStyle.class);
+    private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of(
+            BigTextStyle.class,
+            BigPictureStyle.class,
+            InboxStyle.class,
+            MediaStyle.class,
+            DecoratedCustomViewStyle.class,
+            DecoratedMediaCustomViewStyle.class,
+            MessagingStyle.class,
+            CallStyle.class
+    );
+
+    private static boolean isPlatformStyle(Style style) {
+        if (style == null) {
+            return false;
+        }
+
+        if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) {
+            return true;
+        }
+
+        if (Flags.apiRichOngoing()) {
+            return style.getClass() == ProgressStyle.class;
+        }
+
+        return false;
+    }
 
     /** @hide */
     @IntDef(flag = true, prefix = {"FLAG_"}, value = {
@@ -1620,6 +1641,66 @@
     public static final String EXTRA_COLORIZED = "android.colorized";
 
     /**
+     * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment}
+     * bundles provided by a
+     * {@link android.app.Notification.ProgressStyle} notification as supplied to
+     * {@link ProgressStyle#setProgressSegments}
+     * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}.
+     * This extra is a parcelable array list of bundles.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments";
+
+    /**
+     * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step}
+     * bundles provided by a
+     * {@link android.app.Notification.ProgressStyle} notification as supplied to
+     * {@link ProgressStyle#setProgressSteps}
+     * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}.
+     * This extra is a parcelable array list of bundles.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps";
+
+    /**
+     * {@link #extras} key: whether the progress bar should be styled by its progress as
+     * supplied to {@link ProgressStyle#setStyledByProgress}.
+     * This extra is a boolean.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressTrackerIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressStartIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon";
+
+    /**
+     * {@link #extras} key: this is an {@link Icon} of an image to be
+     * shown at the end of the progress bar in {@link ProgressStyle}, supplied to
+     *{@link ProgressStyle#setProgressEndIcon(Icon)}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon";
+
+    /**
      * @hide
      */
     public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo";
@@ -3072,6 +3153,9 @@
 
         if (Flags.apiRichOngoing()) {
             visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class));
+            visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class));
         }
 
         if (mBubbleMetadata != null) {
@@ -6630,7 +6714,7 @@
             // Custom views which come from a platform style class are safe, and thus do not need to
             // be wrapped.  Any subclass of those styles has the opportunity to make arbitrary
             // changes to the RemoteViews, and thus can't be trusted as a fully vetted view.
-            if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) {
+            if (fromStyle && isPlatformStyle(mStyle)) {
                 return false;
             }
             return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S;
@@ -7971,6 +8055,12 @@
                 return innerClass;
             }
         }
+
+        if (Flags.apiRichOngoing()) {
+            if (templateClass.equals(ProgressStyle.class.getName())) {
+                return ProgressStyle.class;
+            }
+        }
         return null;
     }
 
@@ -11220,6 +11310,724 @@
         }
     }
 
+
+    /**
+     * A Notification Style used to to define a notification whose expanded state includes
+     * a highly customizable progress bar with segments, steps, a custom tracker icon,
+     * and custom icons at the start and end of the progress bar.
+     *
+     * This style is suggested for use cases where the app is showing a tracker to the
+     * user of a thing they are interested in: the location of a car on its way
+     * to pick them up, food being delivered, or their own progress in a navigation
+     * journey.
+     *
+     * To use this style with your Notification, feed it to
+     * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+     * <pre class="prettyprint">
+     * new Notification.Builder(context)
+     *   .setSmallIcon(R.drawable.ic_notification)
+     *   .setColor(Color.GREEN)
+     *   .setColorized(true)
+     *   .setContentTitle("Arrive 10:08 AM").
+     *   .setContentText("Dominique Ansel Bakery Soho")
+     *   .addAction(new Notification.Action("Exit navigation",...))
+     *   .setStyle(new Notification.ProgressStyle()
+     *       .setStyledByProgress(false)
+     *       .setProgress(456)
+     *       .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker))
+     *       .addProgressSegment(new Segment(41).setColor(Color.BLACK))
+     *       .addProgressSegment(new Segment(552).setColor(Color.YELLOW))
+     *       .addProgressSegment(new Segment(253).setColor(Color.YELLOW))
+     *       .addProgressSegment(new Segment(94).setColor(Color.BLUE))
+     *       .addProgressStep(new Step(60).setColor(Color.RED))
+     *       .addProgressStep(new Step(560).setColor(Color.YELLOW))
+     *   )
+     * </pre>
+     *
+     *
+     *
+     * NOTE: The progress bar layout will be mirrored for RTL layout.
+     * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by
+     * the values set on this style object when the notification is built.
+     *
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static class ProgressStyle extends Notification.Style {
+        private static final String KEY_ELEMENT_STABLE_ID = "stableId";
+        private static final String KEY_ELEMENT_COLOR = "colorInt";
+        private static final String KEY_SEGMENT_LENGTH = "length";
+        private static final String KEY_STEP_POSITION = "position";
+
+        private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15;
+        private static final int MAX_PROGRESS_STEP_LIMIT = 5;
+        private static final int DEFAULT_PROGRESS_MAX = 100;
+
+        private List<Segment> mProgressSegments = new ArrayList<>();
+        private List<Step> mProgressSteps = new ArrayList<>();
+
+        private int mProgress = 0;
+
+        private boolean mIndeterminate;
+
+        private boolean mIsStyledByProgress = true;
+
+        @Nullable
+        private Icon mTrackerIcon;
+        @Nullable
+        private Icon mStartIcon;
+        @Nullable
+        private Icon mEndIcon;
+
+        /**
+         * @hide
+         */
+        @Override
+        public boolean areNotificationsVisiblyDifferent(Style other) {
+            if (other == null || getClass() != other.getClass()) {
+                return true;
+            }
+
+            final ProgressStyle progressStyle = (ProgressStyle) other;
+
+            /**
+             * @see #setProgressIndeterminate
+             */
+            if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) {
+                return true;
+            }
+            boolean nonIndeterminateCheckResult = false;
+            if (!mIndeterminate) {
+                nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress)
+                        || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress)
+                        || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments)
+                        || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps)
+                        || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon);
+            }
+
+            return !Objects.equals(mStartIcon, progressStyle.mStartIcon)
+                    || !Objects.equals(mEndIcon, progressStyle.mEndIcon)
+                    || nonIndeterminateCheckResult;
+        }
+
+        /**
+         * Gets the segments that define the background layer of the progress bar.
+         *
+         * If no segments are provided, the progress bar will be rendered with a single segment
+         * with length 100 and default color.
+         *
+         * @see #setProgressSegments
+         * @see #addProgressSegment
+         * @see Segment
+         */
+        public @NonNull List<Segment> getProgressSegments() {
+            return mProgressSegments;
+        }
+
+        /**
+         * Sets or replaces the segments of the progress bar.
+         *
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         * @see Segment
+         */
+        public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) {
+            mProgressSegments = new ArrayList<>(progressSegments.size());
+            return this;
+        }
+
+        /**
+         * Appends a segment to the end of the progress bar.
+         *
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         * @see Segment
+         */
+        public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) {
+            if (mProgressSegments == null) {
+                mProgressSegments = new ArrayList<>();
+            }
+            mProgressSegments.add(segment);
+
+            return this;
+        }
+
+        /**
+         * Gets the steps that are displayed on the progress bar.
+         *.
+         * @see #setProgressSteps
+         * @see #addProgressStep
+         * @see Step
+         */
+        public @NonNull List<Step> getProgressSteps() {
+            return mProgressSteps;
+        }
+
+        /**
+         * Replaces all the progress steps.
+         *
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         * @see Step
+         */
+        public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) {
+            mProgressSteps = new ArrayList<>(steps);
+            return this;
+        }
+
+        /**
+         * Adds another step.
+         *
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         *
+         * Steps can be added in any order, as their
+         * position within the progress bar is determined by their individual
+         * {@link Step#getPosition()}.
+         * @see Step
+         */
+        public @NonNull ProgressStyle addProgressStep(@NonNull Step step) {
+            if (mProgressSteps == null) {
+                mProgressSteps = new ArrayList<>();
+            }
+            mProgressSteps.add(step);
+
+            return this;
+        }
+
+        /**
+         * Gets the progress value of the progress bar.
+         * @see #setProgress
+         */
+        public int getProgress() {
+            return mProgress;
+        }
+
+        /**
+        * Specifies the progress (in the same units as {@link Segment#getLength()})
+        * of the tracker along the length of the bar.
+        *
+        * The max progress value is the sum of all Segment lengths.
+        * The default value is 0.
+        */
+        public @NonNull ProgressStyle setProgress(int progress) {
+            mProgress = progress;
+            return this;
+        }
+
+        /**
+         * Gets the sum of the lengths of all Segments in the style, which
+         * defines the maximum progress. Defaults to 100 when segments are omitted.
+         */
+        public int getProgressMax() {
+            final List<Segment> progressSegment = mProgressSegments;
+            if (progressSegment == null || progressSegment.isEmpty()) {
+                return DEFAULT_PROGRESS_MAX;
+            } else {
+                int progressMax = 0;
+                int validSegmentCount = 0;
+                for (int i = 0; i < progressSegment.size()
+                        && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) {
+                    int segmentLength = progressSegment.get(i).getLength();
+                    if (segmentLength > 0) {
+                        try {
+                            progressMax = Math.addExact(progressMax, segmentLength);
+                            validSegmentCount++;
+                        } catch (ArithmeticException e) {
+                            Log.e(TAG,
+                                    "Notification.ProgressStyle segment total overflowed.", e);
+                            return DEFAULT_PROGRESS_MAX;
+                        }
+                    }
+                }
+
+                if (validSegmentCount == 0) {
+                    return DEFAULT_PROGRESS_MAX;
+                }
+
+                return progressMax;
+            }
+
+        }
+
+        /**
+         * Get indeterminate value of the progress bar.
+         * @see #setProgressIndeterminate
+         */
+        public boolean isProgressIndeterminate() {
+            return mIndeterminate;
+        }
+
+        /**
+         * Used to indicate an initialization state without a known progress amount.
+         * When specified, the following fields are ignored:
+         * @see #setProgress
+         * @see #setProgressSegments
+         * @see #setProgressSteps
+         * @see #setProgressTrackerIcon
+         * @see #setStyledByProgress
+         *
+         * If the app provides exactly one Segment, that segment's color will be
+         * used to style the indeterminate bar.
+         */
+        public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) {
+            mIndeterminate = indeterminate;
+            return this;
+        }
+
+        /**
+         * Gets whether the progress bar's style is based on its progress.
+         * @see #setStyledByProgress
+         */
+        public boolean isStyledByProgress() {
+            return mIsStyledByProgress;
+        }
+
+        /**
+         * Indicates whether the segments and steps will be styled differently
+         * based on whether they are behind or ahead of the current progress.
+         * When true, segments appearing ahead of the current progress will be given a
+         * slightly different appearance to indicate that it is part of the progress bar
+         * that is not "filled".
+         * When false, all segments will be given the filled appearance, and it will be
+         * the app's responsibility to use #setProgressTrackerIcon or segment colors
+         * to make the current progress clear to the user.
+         * the default value is true.
+         */
+        public @NonNull ProgressStyle setStyledByProgress(boolean enabled) {
+            mIsStyledByProgress = enabled;
+            return this;
+        }
+
+
+        /**
+         * Gets the progress tracker icon for the progress bar.
+         * @see #setProgressTrackerIcon
+         */
+        public @Nullable Icon getProgressTrackerIcon() {
+            return mTrackerIcon;
+        }
+
+        /**
+         * An optional icon that can appear as an overlay on the bar at the point of
+         * current progress.
+         * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that
+         * aspect ratio range will be cropped.
+         * This icon will be mirrored in RTL.
+         */
+        public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) {
+            mTrackerIcon = trackerIcon;
+            return this;
+        }
+
+        /**
+         * Gets the progress bar start icon.
+         * @see #setProgressStartIcon
+         */
+        public @Nullable Icon getProgressStartIcon() {
+            return mStartIcon;
+        }
+
+        /**
+         * An optional square icon that appears at the start of the progress bar.
+         * This icon will be cropped to its central square.
+         * This icon will NOT be mirrored in RTL layouts.
+         */
+        public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) {
+            mStartIcon = startIcon;
+            return this;
+        }
+
+        /**
+         * Gets the progress bar end icon.
+         * @see #setProgressEndIcon(Icon)
+         */
+        public @Nullable Icon getProgressEndIcon() {
+            return mEndIcon;
+        }
+
+        /**
+         * An optional square icon that appears at the end of the progress bar.
+         * This icon will be cropped to its central square.
+         * This icon will NOT be mirrored in RTL layouts.
+         */
+        public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) {
+            mEndIcon = endIcon;
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void purgeResources() {
+            super.purgeResources();
+            if (mTrackerIcon != null) {
+                mTrackerIcon.convertToAshmem();
+            }
+            if (mStartIcon != null) {
+                mStartIcon.convertToAshmem();
+            }
+            if (mEndIcon != null) {
+                mEndIcon.convertToAshmem();
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void reduceImageSizes(Context context) {
+            super.reduceImageSizes(context);
+
+            final Resources resources = context.getResources();
+
+            int progressIconSize =
+                    resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size);
+            if (mStartIcon != null) {
+                mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+            }
+            if (mEndIcon != null) {
+                mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+            }
+            if (mTrackerIcon != null) {
+                int progressTrackerWidth = resources.getDimensionPixelSize(
+                        R.dimen.notification_progress_tracker_width);
+                int progressTrackerHeight = resources.getDimensionPixelSize(
+                        R.dimen.notification_progress_tracker_height);
+                mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void addExtras(Bundle extras) {
+            super.addExtras(extras);
+            extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS,
+                    getProgressSegmentsAsBundleList(mProgressSegments));
+            extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS,
+                    getProgressStepsAsBundleList(mProgressSteps));
+
+            extras.putInt(EXTRA_PROGRESS, mProgress);
+            extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate);
+            extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax());
+            extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress);
+
+            if (mTrackerIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_TRACKER_ICON);
+            }
+
+            if (mStartIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_START_ICON);
+            }
+
+            if (mEndIcon != null) {
+                extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon);
+            } else {
+                extras.remove(EXTRA_PROGRESS_END_ICON);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        protected void restoreFromExtras(Bundle extras) {
+            super.restoreFromExtras(extras);
+            mProgressSegments = getProgressSegmentsFromBundleList(
+                    extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class));
+            mProgress = extras.getInt(EXTRA_PROGRESS, 0);
+            mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false);
+            mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true);
+            mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class);
+            mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class);
+            mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class);
+            mProgressSteps = getProgressStepsFromBundleList(
+                    extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class));
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public boolean displayCustomViewInline() {
+            // This is a lie; True is returned for progress notifications to make sure
+            // that the custom view is not used instead of the template, but it will not
+            // actually be included.
+            return true;
+        }
+
+        private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
+                @Nullable List<Segment> progressSegments) {
+            final ArrayList<Bundle> segments = new ArrayList<>();
+            if (progressSegments != null && !progressSegments.isEmpty()) {
+                for (int i = 0; i < progressSegments.size(); i++) {
+                    final Segment segment = progressSegments.get(i);
+                    if (segment.getLength() <= 0) {
+                        continue;
+                    }
+
+                    final Bundle bundle = new Bundle();
+                    bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength());
+                    bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId());
+                    bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor());
+
+                    segments.add(bundle);
+                }
+            }
+
+            return segments;
+        }
+
+        private static @NonNull List<Segment> getProgressSegmentsFromBundleList(
+                @Nullable List<Bundle> segmentBundleList) {
+            final ArrayList<Segment> segments = new ArrayList<>();
+            if (segmentBundleList != null && !segmentBundleList.isEmpty()) {
+                for (int i = 0; i < segmentBundleList.size(); i++) {
+                    final Bundle segmentBundle = segmentBundleList.get(i);
+                    final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH);
+                    if (length <= 0) {
+                        continue;
+                    }
+
+                    final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+                    final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+                            Notification.COLOR_DEFAULT);
+                    final Segment segment = new Segment(length)
+                            .setStableId(stableId).setColor(color);
+
+                    segments.add(segment);
+                }
+            }
+
+            return segments;
+        }
+
+        private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList(
+                @Nullable List<Step> progressSteps) {
+            final ArrayList<Bundle> steps = new ArrayList<>();
+            if (progressSteps != null && !progressSteps.isEmpty()) {
+                for (int i = 0; i < progressSteps.size(); i++) {
+                    final Step step = progressSteps.get(i);
+                    if (step.getPosition() < 0) {
+                        continue;
+                    }
+
+                    final Bundle bundle = new Bundle();
+                    bundle.putInt(KEY_STEP_POSITION, step.getPosition());
+                    bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId());
+                    bundle.putInt(KEY_ELEMENT_COLOR, step.getColor());
+
+                    steps.add(bundle);
+                }
+            }
+
+            return steps;
+        }
+
+        private static @NonNull List<Step> getProgressStepsFromBundleList(
+                @Nullable List<Bundle> stepBundleList) {
+            final ArrayList<Step> steps = new ArrayList<>();
+
+            if (stepBundleList != null && !stepBundleList.isEmpty()) {
+                for (int i = 0; i < stepBundleList.size(); i++) {
+                    final Bundle segmentBundle = stepBundleList.get(i);
+                    final int position = segmentBundle.getInt(KEY_STEP_POSITION);
+                    if (position < 0) {
+                        continue;
+                    }
+                    final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+                    final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+                            Notification.COLOR_DEFAULT);
+                    final Step step = new Step(position).setStableId(stableId).setColor(color);
+                    steps.add(step);
+                }
+            }
+
+            return steps;
+        }
+
+        /**
+         * A segment of the progress bar, which defines its length and color.
+         * Segments allow for creating progress bars with multiple colors or sections
+         * to represent different stages or categories of progress.
+         * For example, Traffic conditions along a navigation journey.
+         */
+        public static final class Segment {
+            private int mLength;
+            private int mStableId = 0;
+            @ColorInt
+            private int mColor = Notification.COLOR_DEFAULT;
+
+            /**
+             * Create a segment with a non-zero length.
+             * @param length
+             * See {@link #getLength}
+             */
+            public Segment(int length) {
+                mLength = length;
+            }
+
+            /**
+             * The length of this Segment within the progress bar.
+             * This value has no units, it is just relative to the length of other segments,
+             * and the value provided to {@link ProgressStyle#setProgress}.
+             */
+            public int getLength() {
+                return mLength;
+            }
+
+            /**
+             * Gets the stable id of this Segment.
+             *
+             * @see #setStableId
+             */
+            public int getStableId() {
+                return mStableId;
+            }
+
+            /**
+             * Optional ID used to uniquely identify the element across updates.
+             */
+            public @NonNull Segment setStableId(int stableId) {
+                mStableId = stableId;
+                return this;
+            }
+
+            /**
+             * Returns the color of this Segment.
+             *
+             * @see #setColor
+             */
+            @ColorInt
+            public int getColor() {
+                return mColor;
+            }
+
+            /**
+             * Optional color of this Segment
+             */
+            public @NonNull Segment setColor(@ColorInt int color) {
+                mColor = color;
+                return this;
+            }
+
+            /**
+             * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+             */
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                Segment segment = (Segment) o;
+                return mLength == segment.mLength && mStableId == segment.mStableId
+                        && mColor == segment.mColor;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mLength, mStableId, mColor);
+            }
+        }
+
+        /**
+         * A step within the progress bar, defining its position and color.
+         * Steps are designated points within a progressbar to visualize
+         * distinct stages or milestones.
+         * For example, you might use steps to mark stops in a multi-stop
+         * navigation journey, where each step represents a destination.
+         */
+        public static final class Step {
+
+            private int mPosition;
+            private int mStableId;
+            @ColorInt
+            private int mColor = Notification.COLOR_DEFAULT;
+
+            /**
+             * Create a step element.
+             * The position of this step on the progress bar
+             * relative to {@link ProgressStyle#getProgressMax}
+             * @param position
+             * See {@link #getPosition}
+             */
+            public Step(int position) {
+                mPosition = position;
+            }
+
+            /**
+             * Gets the position of this Step.
+             * The position of this step on the progress bar
+             * relative to {@link ProgressStyle#getProgressMax}.
+             */
+            public int getPosition() {
+                return mPosition;
+            }
+
+
+            /**
+             * Optional ID used to uniqurely identify the element across updates.
+             */
+            public int getStableId() {
+                return mStableId;
+            }
+
+            /**
+             * Optional ID used to uniqurely identify the element across updates.
+             */
+            public @NonNull Step setStableId(int stableId) {
+                mStableId = stableId;
+                return this;
+            }
+
+            /**
+             * Returns the color of this Segment.
+             *
+             * @see #setColor
+             */
+            @ColorInt
+            public int getColor() {
+                return mColor;
+            }
+
+            /**
+             * Optional color of this Segment
+             */
+            public @NonNull Step setColor(@ColorInt int color) {
+                mColor = color;
+                return this;
+            }
+
+            /**
+             * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+             */
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                Step step = (Step) o;
+                return mPosition == step.mPosition && mStableId == step.mStableId
+                        && mColor == step.mColor;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mPosition, mStableId, mColor);
+            }
+        }
+    }
+
     /**
      * Notification style for custom views that are decorated by the system
      *
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 4682f3d..216ba5d 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,8 @@
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
 import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.ICancellationSignal;
 import android.os.RemoteException;
 
 import java.util.Objects;
@@ -73,7 +75,43 @@
      *     android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
      *     android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
      *     ExecuteAppFunctionResponse.RESULT_DENIED}.
+     * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+     *     CancellationSignal, Consumer)} instead. This method will be removed once usage references
+     *     are updated.
      */
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
+                Manifest.permission.EXECUTE_APP_FUNCTIONS
+            },
+            conditional = true)
+    @UserHandleAware
+    @Deprecated
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        executeAppFunction(request, executor, new CancellationSignal(), callback);
+    }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Note: Applications can execute functions they define. To execute functions defined in
+     * another component, apps would need to have {@code
+     * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+     * android.permission.EXECUTE_APP_FUNCTIONS}.
+     *
+     * @param request the request to execute the app function
+     * @param executor the executor to run the callback
+     * @param cancellationSignal the cancellation signal to cancel the execution.
+     * @param callback the callback to receive the function execution result. if the calling app
+     *     does not own the app function or does not have {@code
+     *     android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+     *     android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
+     *     ExecuteAppFunctionResponse.RESULT_DENIED}.
+     */
+    // TODO(b/357551503): Document the behavior when the cancellation signal is issued.
     // TODO(b/360864791): Document that apps can opt-out from being executed by callers with
     //   EXECUTE_APP_FUNCTIONS and how a caller knows whether a function is opted out.
     // TODO(b/357551503): Update documentation when get / set APIs are implemented that this will
@@ -88,6 +126,7 @@
     public void executeAppFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull @CallbackExecutor Executor executor,
+            @NonNull CancellationSignal cancellationSignal,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
         Objects.requireNonNull(request);
         Objects.requireNonNull(executor);
@@ -96,25 +135,31 @@
         ExecuteAppFunctionAidlRequest aidlRequest =
                 new ExecuteAppFunctionAidlRequest(
                         request, mContext.getUser(), mContext.getPackageName());
+
         try {
-            mService.executeAppFunction(
-                    aidlRequest,
-                    new IExecuteAppFunctionCallback.Stub() {
-                        @Override
-                        public void onResult(ExecuteAppFunctionResponse result) {
-                            try {
-                                executor.execute(() -> callback.accept(result));
-                            } catch (RuntimeException e) {
-                                // Ideally shouldn't happen since errors are wrapped into the
-                                // response, but we catch it here for additional safety.
-                                callback.accept(
-                                        ExecuteAppFunctionResponse.newFailure(
-                                                getResultCode(e),
-                                                e.getMessage(),
-                                                /* extras= */ null));
-                            }
-                        }
-                    });
+            ICancellationSignal cancellationTransport =
+                    mService.executeAppFunction(
+                            aidlRequest,
+                            new IExecuteAppFunctionCallback.Stub() {
+                                @Override
+                                public void onResult(ExecuteAppFunctionResponse result) {
+                                    try {
+                                        executor.execute(() -> callback.accept(result));
+                                    } catch (RuntimeException e) {
+                                        // Ideally shouldn't happen since errors are wrapped into
+                                        // the
+                                        // response, but we catch it here for additional safety.
+                                        callback.accept(
+                                                ExecuteAppFunctionResponse.newFailure(
+                                                        getResultCode(e),
+                                                        e.getMessage(),
+                                                        /* extras= */ null));
+                                    }
+                                }
+                            });
+            if (cancellationTransport != null) {
+                cancellationSignal.setRemote(cancellationTransport);
+            }
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 0d981ea..8e41773 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -29,7 +29,12 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.CancellationSignal;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
 
 import java.util.function.Consumer;
 
@@ -74,6 +79,7 @@
          */
         void perform(
                 @NonNull ExecuteAppFunctionRequest request,
+                @NonNull CancellationSignal cancellationSignal,
                 @NonNull Consumer<ExecuteAppFunctionResponse> callback);
     }
 
@@ -85,6 +91,7 @@
             @Override
             public void executeAppFunction(
                     @NonNull ExecuteAppFunctionRequest request,
+                    @NonNull ICancellationCallback cancellationCallback,
                     @NonNull IExecuteAppFunctionCallback callback) {
                 if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
                         == PERMISSION_DENIED) {
@@ -93,7 +100,10 @@
                 SafeOneTimeExecuteAppFunctionCallback safeCallback =
                         new SafeOneTimeExecuteAppFunctionCallback(callback);
                 try {
-                    onExecuteFunction.perform(request, safeCallback::onResult);
+                    onExecuteFunction.perform(
+                            request,
+                            buildCancellationSignal(cancellationCallback),
+                            safeCallback::onResult);
                 } catch (Exception ex) {
                     // Apps should handle exceptions. But if they don't, report the error on
                     // behalf of them.
@@ -105,6 +115,21 @@
         };
     }
 
+    private static CancellationSignal buildCancellationSignal(
+            @NonNull ICancellationCallback cancellationCallback) {
+        final ICancellationSignal cancellationSignalTransport =
+                CancellationSignal.createTransport();
+        CancellationSignal cancellationSignal =
+                CancellationSignal.fromTransport(cancellationSignalTransport);
+        try {
+            cancellationCallback.sendCancellationTransport(cancellationSignalTransport);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return cancellationSignal ;
+    }
+
     private final Binder mBinder = createBinder(
             AppFunctionService.this,
             AppFunctionService.this::onExecuteFunction);
@@ -115,6 +140,7 @@
         return mBinder;
     }
 
+
     /**
      * Called by the system to execute a specific app function.
      *
@@ -134,9 +160,45 @@
      *
      * @param request The function execution request.
      * @param callback A callback to report back the result.
+     *
+     * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+     *     Consumer)} instead. This method will be removed once usage references are updated.
      */
     @MainThread
+    @Deprecated
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
+     * the execution of function if requested by the system.
+     *
+     * @param request The function execution request.
+     * @param cancellationSignal A signal to cancel the execution.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        onExecuteFunction(request, callback);
+    }
 }
diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
index 84b1837..b29b64e 100644
--- a/core/java/android/app/appfunctions/GenericDocumentWrapper.java
+++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
@@ -16,10 +16,13 @@
 
 package android.app.appfunctions;
 
+import android.annotation.Nullable;
 import android.app.appsearch.GenericDocument;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.MathUtils;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 
 import java.util.Objects;
@@ -31,24 +34,33 @@
  * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder
  * directly or Android shared memory if the data is large.
  *
+ * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled
+ * from the underlying `Parcel` when {@link #getValue()} is called. This optimization
+ * allows the system server to pass through the generic document, without unparcel and parcel it.
+ *
  * @hide
  * @see Parcel#writeBlob(byte[])
  */
 public final class GenericDocumentWrapper implements Parcelable {
+    @Nullable
+    @GuardedBy("mLock")
+    private GenericDocument mGenericDocument;
+    @GuardedBy("mLock")
+    @Nullable private Parcel mParcel;
+    private final Object mLock = new Object();
+
     public static final Creator<GenericDocumentWrapper> CREATOR =
             new Creator<>() {
                 @Override
                 public GenericDocumentWrapper createFromParcel(Parcel in) {
-                    byte[] dataBlob = Objects.requireNonNull(in.readBlob());
-                    Parcel unmarshallParcel = Parcel.obtain();
-                    try {
-                        unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
-                        unmarshallParcel.setDataPosition(0);
-                        return new GenericDocumentWrapper(
-                                GenericDocument.createFromParcel(unmarshallParcel));
-                    } finally {
-                        unmarshallParcel.recycle();
-                    }
+                    int length = in.readInt();
+                    int offset = in.dataPosition();
+                    in.setDataPosition(MathUtils.addOrThrow(offset, length));
+
+                    Parcel p = Parcel.obtain();
+                    p.appendFrom(in, offset, length);
+                    p.setDataPosition(0);
+                    return new GenericDocumentWrapper(p);
                 }
 
                 @Override
@@ -56,16 +68,42 @@
                     return new GenericDocumentWrapper[size];
                 }
             };
-    @NonNull private final GenericDocument mGenericDocument;
 
     public GenericDocumentWrapper(@NonNull GenericDocument genericDocument) {
         mGenericDocument = Objects.requireNonNull(genericDocument);
+        mParcel = null;
+    }
+
+    public GenericDocumentWrapper(@NonNull Parcel parcel) {
+        mGenericDocument = null;
+        mParcel = Objects.requireNonNull(parcel);
     }
 
     /** Returns the wrapped {@link android.app.appsearch.GenericDocument} */
     @NonNull
     public GenericDocument getValue() {
-        return mGenericDocument;
+        unparcel();
+        synchronized (mLock) {
+            return Objects.requireNonNull(mGenericDocument);
+        }
+    }
+
+    private void unparcel() {
+        synchronized (mLock) {
+            if (mGenericDocument != null) {
+                return;
+            }
+            byte[] dataBlob = Objects.requireNonNull(Objects.requireNonNull(mParcel).readBlob());
+            Parcel unmarshallParcel = Parcel.obtain();
+            try {
+                unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+                unmarshallParcel.setDataPosition(0);
+                mGenericDocument = GenericDocument.createFromParcel(unmarshallParcel);
+                mParcel = null;
+            } finally {
+                unmarshallParcel.recycle();
+            }
+        }
     }
 
     @Override
@@ -75,13 +113,32 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        Parcel parcel = Parcel.obtain();
-        try {
-            mGenericDocument.writeToParcel(parcel, flags);
-            byte[] bytes = parcel.marshall();
-            dest.writeBlob(bytes);
-        } finally {
-            parcel.recycle();
+        synchronized (mLock) {
+            if (mGenericDocument != null) {
+                int lengthPos = dest.dataPosition();
+                // write a placeholder for length
+                dest.writeInt(-1);
+                Parcel tempParcel = Parcel.obtain();
+                byte[] bytes;
+                try {
+                    mGenericDocument.writeToParcel(tempParcel, flags);
+                    bytes = tempParcel.marshall();
+                } finally {
+                    tempParcel.recycle();
+                }
+                int startPos = dest.dataPosition();
+                dest.writeBlob(bytes);
+                int endPos = dest.dataPosition();
+                dest.setDataPosition(lengthPos);
+                // Overwrite the length placeholder
+                dest.writeInt(endPos - startPos);
+                dest.setDataPosition(endPos);
+
+            } else {
+                Parcel originalParcel = Objects.requireNonNull(mParcel);
+                dest.writeInt(originalParcel.dataSize());
+                dest.appendFrom(originalParcel, 0, originalParcel.dataSize());
+            }
         }
     }
 }
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index 28827bb..c63217f 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -18,6 +18,7 @@
 
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.os.ICancellationSignal;
 
 /**
  * Defines the interface for apps to interact with the app function execution service
@@ -32,8 +33,8 @@
     * @param callback the callback to report the result.
     */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)")
-    void executeAppFunction(
+    ICancellationSignal executeAppFunction(
         in ExecuteAppFunctionAidlRequest request,
         in IExecuteAppFunctionCallback callback
     );
-}
\ No newline at end of file
+}
diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
index cc5a20c..291f33c 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -16,7 +16,7 @@
 
 package android.app.appfunctions;
 
-import android.os.Bundle;
+import android.app.appfunctions.ICancellationCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.ExecuteAppFunctionRequest;
 
@@ -34,10 +34,12 @@
      * Called by the system to execute a specific app function.
      *
      * @param request  the function execution request.
+     * @param cancellationCallback a callback to send back the cancellation transport.
      * @param callback a callback to report back the result.
      */
     void executeAppFunction(
         in ExecuteAppFunctionRequest request,
+        in ICancellationCallback cancellationCallback,
         in IExecuteAppFunctionCallback callback
     );
 }
diff --git a/core/java/android/app/appfunctions/ICancellationCallback.aidl b/core/java/android/app/appfunctions/ICancellationCallback.aidl
new file mode 100644
index 0000000..03235ac
--- /dev/null
+++ b/core/java/android/app/appfunctions/ICancellationCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appfunctions;
+
+import android.os.ICancellationSignal;
+
+/** {@hide} */
+oneway interface ICancellationCallback {
+    void sendCancellationTransport(in ICancellationSignal cancellationTransport);
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 2162792..1b21bdf 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -181,7 +181,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE;
 
     /**
@@ -191,7 +191,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT =
             ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT;
 
@@ -201,7 +201,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public static final int ROTATION_OVERRIDE_ROTATION_ONLY =
             ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY;
 
@@ -1562,7 +1562,7 @@
      */
     public static int getRotationOverride(@Nullable Context context,
             @Nullable PackageManager packageManager, @Nullable String packageName) {
-        if (com.android.window.flags.Flags.cameraCompatForFreeform()) {
+        if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) {
             return getRotationOverrideInternal(context, packageManager, packageName);
         } else {
             return shouldOverrideToPortrait(packageManager, packageName)
@@ -1574,7 +1574,7 @@
     /**
      * @hide
      */
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @TestApi
     public static int getRotationOverrideInternal(@Nullable Context context,
             @Nullable PackageManager packageManager, @Nullable String packageName) {
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 79cbd19..05e91e4 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -1529,6 +1529,8 @@
          * config that can be used by
          * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)}
          *
+         * @deprecated should use builder-based constructor instead.
+         *             TODO(b/368042125): remove this method.
          * @param captureRequested Whether the DSP should capture the trigger sound.
          * @param allowMultipleTriggers Whether the service should restart listening after the DSP
          *                              triggers.
@@ -1539,6 +1541,8 @@
          *
          * @hide
          */
+        @Deprecated
+        @SuppressWarnings("Todo")
         @TestApi
         public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
                 @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
@@ -1695,6 +1699,89 @@
             result = prime * result + mAudioCapabilities;
             return result;
         }
+
+        /**
+         * Builder class for {@link RecognitionConfig} objects.
+         */
+        public static final class Builder {
+            private boolean mCaptureRequested;
+            private boolean mAllowMultipleTriggers;
+            @Nullable private KeyphraseRecognitionExtra[] mKeyphrases;
+            @Nullable private byte[] mData;
+            private int mAudioCapabilities;
+
+            /**
+             * Constructs a new Builder with the default values.
+             */
+            public Builder() {
+            }
+
+            /**
+             * Sets capture requested state.
+             * @param captureRequested The new requested state.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setCaptureRequested(boolean captureRequested) {
+                mCaptureRequested = captureRequested;
+                return this;
+            }
+
+            /**
+             * Sets allow multiple triggers state.
+             * @param allowMultipleTriggers The new allow multiple triggers state.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setAllowMultipleTriggers(boolean allowMultipleTriggers) {
+                mAllowMultipleTriggers = allowMultipleTriggers;
+                return this;
+            }
+
+            /**
+             * Sets the keyphrases field.
+             * @param keyphrases The new keyphrases.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setKeyphrases(
+                    @NonNull Collection<KeyphraseRecognitionExtra> keyphrases) {
+                mKeyphrases = keyphrases.toArray(new KeyphraseRecognitionExtra[keyphrases.size()]);
+                return this;
+            }
+
+            /**
+             * Sets the data field.
+             * @param data The new data.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setData(@Nullable byte[] data) {
+                mData = data;
+                return this;
+            }
+
+            /**
+             * Sets the audio capabilities field.
+             * @param audioCapabilities The new audio capabilities.
+             * @return the same Builder instance.
+             */
+            public @NonNull Builder setAudioCapabilities(int audioCapabilities) {
+                mAudioCapabilities = audioCapabilities;
+                return this;
+            }
+
+            /**
+             * Combines all of the parameters that have been set and return a new
+             * {@link RecognitionConfig} object.
+             * @return a new {@link RecognitionConfig} object
+             */
+            public @NonNull RecognitionConfig build() {
+                RecognitionConfig config = new RecognitionConfig(
+                        /* captureRequested= */ mCaptureRequested,
+                        /* allowMultipleTriggers= */ mAllowMultipleTriggers,
+                        /* keyphrases= */ mKeyphrases,
+                        /* data= */ mData,
+                        /* audioCapabilities= */ mAudioCapabilities);
+                return config;
+            }
+        };
     }
 
     /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b8a8be1..d82af55 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10750,6 +10750,16 @@
                 "lock_screen_show_only_unseen_notifications";
 
         /**
+         * Indicates whether to minimalize the number of notifications to show on the lockscreen.
+         * <p>
+         * Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM =
+                "lock_screen_notification_minimalism";
+
+        /**
          * Indicates whether snooze options should be shown on notifications
          * <p>
          * Type: int (0 for false, 1 for true)
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 45e9def..5457bbe 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -24,6 +24,17 @@
 }
 
 flag {
+  name: "asm_reintroduce_grace_period"
+  namespace: "responsible_apis"
+  description: "Allow launches within the grace period for ASM apps"
+  bug: "367702727"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+
+flag {
     name: "content_uri_permission_apis"
     is_exported: true
     namespace: "responsible_apis"
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index dda3993..d5ccca9 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -157,10 +157,10 @@
             // which prevents overlap with the DisplayCutout.
             if (!attachedInParent && !floatingInScreenWindow) {
                 mTempRect.set(outParentFrame);
-                outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+                intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars);
                 frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
             }
-            outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+            intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars);
         }
 
         final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
@@ -283,6 +283,19 @@
                 + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
     }
 
+    /**
+     * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect
+     * to the side (or the corner) that the other rectangle is away from.
+     * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle
+     * is valid and contained in inOutRect if rectangles involved are valid.
+     */
+    private static void intersectOrClamp(Rect inOutRect, Rect other) {
+        inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right);
+        inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom);
+        inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left);
+        inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top);
+    }
+
     public static void extendFrameByCutout(Rect displayCutoutSafe,
             Rect displayFrame, Rect inOutFrame, Rect tempRect) {
         if (displayCutoutSafe.contains(inOutFrame)) {
diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
index c92593f..7b6e070 100644
--- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
@@ -880,10 +880,6 @@
      * @hide
      */
     public static String typeToString(int type) {
-        if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
-            return "TYPE_WINDOW_CONTROL";
-        }
-
         switch (type) {
             case TYPE_APPLICATION: {
                 return "TYPE_APPLICATION";
@@ -903,8 +899,12 @@
             case TYPE_MAGNIFICATION_OVERLAY: {
                 return "TYPE_MAGNIFICATION_OVERLAY";
             }
-            default:
+            default: {
+                if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
+                    return "TYPE_WINDOW_CONTROL";
+                }
                 return "<UNKNOWN:" + type + ">";
+            }
         }
     }
 
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index e7f0560..258832e 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -157,10 +157,8 @@
     option (.android.msg_privacy).dest = DEST_AUTOMATIC;
     repeated int32 vibrator_ids = 1;
     optional VibrationProto current_vibration = 2;
-    optional bool is_vibrating = 3;
     optional int32 is_vibrator_controller_registered = 27;
     optional VibrationProto current_external_vibration = 4;
-    optional bool vibrator_under_external_control = 5;
     optional bool low_power_mode = 6;
     optional bool vibrate_on = 24;
     reserved 25; // prev keyboard_vibration_on
@@ -183,4 +181,6 @@
     repeated VibrationProto previous_vibrations = 16;
     repeated VibrationParamProto previous_vibration_params = 28;
     reserved 17; // prev previous_external_vibrations
+    reserved 3; // prev is_vibrating, check current_vibration instead
+    reserved 5; // prev vibrator_under_external_control, check current_external_vibration instead
 }
\ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 07efad8..92c3906 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4457,6 +4457,11 @@
     <!-- Bytes that the PinnerService will pin for WebView -->
     <integer name="config_pinnerWebviewPinBytes">0</integer>
 
+    <!-- Maximum memory that PinnerService will pin for apps expressed
+         as a percentage of total device memory [0,100].
+         Example: 10, means 10% of total memory will be the maximum pinned memory -->
+    <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer>
+
     <!-- Number of days preloaded file cache should be preserved on a device before it can be
          deleted -->
     <integer name="config_keepPreloadsMinDays">7</integer>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index f397ef2..6683dc0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -808,6 +808,12 @@
          This is bigger than displayed because listeners can use it for other displays
          e.g. wearables. -->
     <dimen name="notification_person_icon_max_size">144dp</dimen>
+    <!-- The size of the progress bar icon -->
+    <dimen name="notification_progress_icon_size">20dp</dimen>
+    <!-- The size of the progress tracker width -->
+    <dimen name="notification_progress_tracker_width">40dp</dimen>
+    <!-- The size of the progress tracker height -->
+    <dimen name="notification_progress_tracker_height">20dp</dimen>
 
     <!-- The maximum size of the small notification icon on low memory devices. -->
     <dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 06b36b8..5f40a6c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3467,6 +3467,7 @@
   <java-symbol type="integer" name="config_pinnerHomePinBytes" />
   <java-symbol type="bool" name="config_pinnerAssistantApp" />
   <java-symbol type="integer" name="config_pinnerWebviewPinBytes" />
+  <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" />
 
   <java-symbol type="string" name="config_doubleTouchGestureEnableFile" />
 
@@ -3854,6 +3855,9 @@
   <java-symbol type="dimen" name="notification_custom_view_max_image_height"/>
   <java-symbol type="dimen" name="notification_custom_view_max_image_width"/>
   <java-symbol type="dimen" name="notification_person_icon_max_size" />
+  <java-symbol type="dimen" name="notification_progress_icon_size" />
+  <java-symbol type="dimen" name="notification_progress_tracker_width" />
+  <java-symbol type="dimen" name="notification_progress_tracker_height" />
 
   <java-symbol type="dimen" name="notification_small_icon_size_low_ram"/>
   <java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/>
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0f73df9..edcea24 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -97,7 +97,6 @@
 import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.Pair;
-import android.util.Slog;
 import android.widget.RemoteViews;
 
 import androidx.test.InstrumentationRegistry;
@@ -118,6 +117,7 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -2114,6 +2114,300 @@
         assertThat(n.getWhen()).isEqualTo(9);
     }
 
+    @Test
+    public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() {
+        final List<Class<? extends Notification.Style>> platformStyleClasses = List.of(
+                Notification.BigTextStyle.class, Notification.BigPictureStyle.class,
+                Notification.MessagingStyle.class, Notification.CallStyle.class,
+                Notification.InboxStyle.class, Notification.MediaStyle.class,
+                Notification.DecoratedCustomViewStyle.class,
+                Notification.DecoratedMediaCustomViewStyle.class
+        );
+
+        for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) {
+            assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName()))
+                    .isEqualTo(platformStyleClass);
+        }
+    }
+
+    @Test
+    public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() {
+        assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName()))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_richOngoingEnabled_platformClass() {
+        assertThat(
+                Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+                .isEqualTo(Notification.ProgressStyle.class);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_richOngoingDisabled_notPlatformClass() {
+        assertThat(
+                Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onSegmentChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.RED)));
+
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.BLUE)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.RED)));
+
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+                        .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+                                .setColor(Color.BLUE)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onStartIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onEndIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onProgressChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgress(20));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgress(21));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgress(20));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgress(21));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setStyledByProgress(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setStyledByProgress(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setStyledByProgress(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setStyledByProgress(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onProgressStepChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressStep(new Notification.ProgressStyle.Step(10)));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressStep(new Notification.ProgressStyle.Step(10)));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onTrackerIconChange_visiblyDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() {
+        final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+        final Icon icon2 = Icon.createWithBitmap(
+                Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+                        .setProgressTrackerIcon(icon1));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle()
+                        .setProgressIndeterminate(true).setProgressTrackerIcon(icon2));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_onIndeterminateChange_visiblyDifferent() {
+        final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true));
+        final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false));
+
+        assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+                .isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_default100() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_nooSegments_returnsDefault() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle.setProgressSegments(Collections.emptyList());
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_returnsSumOfSegmentLength() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle
+                .addProgressSegment(new Notification.ProgressStyle.Segment(10))
+                .addProgressSegment(new Notification.ProgressStyle.Segment(20));
+
+        assertThat(progressStyle.getProgressMax()).isEqualTo(30);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() {
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle
+                .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE))
+                .addProgressSegment(new Notification.ProgressStyle.Segment(10));
+
+        assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_indeterminate_defaultValueFalse() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+        assertThat(progressStyle1.isProgressIndeterminate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_styledByProgress_defaultValueTrue() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+        assertThat(progressStyle1.isStyledByProgress()).isTrue();
+    }
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
@@ -2214,4 +2508,11 @@
                 new Intent(action).setPackage(mContext.getPackageName()),
                 PendingIntent.FLAG_MUTABLE);
     }
+
+    private static class NotAPlatformStyle extends Notification.Style {
+        @Override
+        public boolean areNotificationsVisiblyDifferent(Notification.Style other) {
+            return false;
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
index 5cac98d..d4693e6 100644
--- a/core/tests/coretests/src/android/view/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -413,4 +413,19 @@
         assertInsetByTopBottom(0, 0, mFrames.parentFrame);
         assertInsetByTopBottom(0, 0, mFrames.frame);
     }
+
+    @Test
+    public void windowBoundsOutsideDisplayCutoutSafe() {
+        addDisplayCutout();
+        mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+        mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0);
+        computeFrames();
+
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.displayFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.parentFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.frame);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 37f0067..0896138 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,12 +16,11 @@
 
 package androidx.window.common;
 
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 
 import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
 import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
 
-import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.devicestate.DeviceState;
 import android.hardware.devicestate.DeviceStateManager;
@@ -31,16 +30,23 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
+import androidx.annotation.BinderThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.common.layout.DisplayFoldFeatureCommon;
 
 import com.android.internal.R;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -55,13 +61,6 @@
     private static final boolean DEBUG = false;
 
     /**
-     * Emulated device state
-     * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
-     * {@link CommonFoldingFeature.State} map.
-     */
-    private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
-
-    /**
      * Device state received via
      * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
      * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
@@ -71,23 +70,40 @@
      * "rear display". Concurrent mode for example is activated via public API and can be active in
      * both the "open" and "half folded" device states.
      */
-    private DeviceState mCurrentDeviceState = new DeviceState(
-            new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
-                    "INVALID").build());
+    // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup.
+    private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
 
-    private List<DeviceState> mSupportedStates;
+    /**
+     * Lock to synchronize access to {@link #mCurrentDeviceState}.
+     *
+     * <p>This lock is used to ensure thread-safety when accessing and modifying the
+     * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if
+     * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if
+     * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and
+     * ensure data consistency.
+     */
+    private final Object mCurrentDeviceStateLock = new Object();
 
     @NonNull
     private final RawFoldingFeatureProducer mRawFoldSupplier;
 
-    private final boolean mIsHalfOpenedSupported;
+    @NonNull
+    private final DeviceStateMapper mDeviceStateMapper;
 
-    private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+    @VisibleForTesting
+    final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+        // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
+        // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
+        @SuppressWarnings("GuardedBy")
+        @BinderThread // When Flags.wlinfoOncreate() is enabled.
+        @MainThread // When Flags.wlinfoOncreate() is disabled.
         @Override
         public void onDeviceStateChanged(@NonNull DeviceState state) {
-            mCurrentDeviceState = state;
-            mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
-                    .this::notifyFoldingFeatureChange);
+            synchronized (mCurrentDeviceStateLock) {
+                mCurrentDeviceState = state;
+                mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
+                        ::notifyFoldingFeatureChangeLocked);
+            }
         }
     };
 
@@ -95,41 +111,14 @@
             @NonNull RawFoldingFeatureProducer rawFoldSupplier,
             @NonNull DeviceStateManager deviceStateManager) {
         mRawFoldSupplier = rawFoldSupplier;
-        String[] deviceStatePosturePairs = context.getResources()
-                .getStringArray(R.array.config_device_state_postures);
-        mSupportedStates = deviceStateManager.getSupportedDeviceStates();
-        boolean isHalfOpenedSupported = false;
-        for (String deviceStatePosturePair : deviceStatePosturePairs) {
-            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
-            if (deviceStatePostureMapping.length != 2) {
-                if (DEBUG) {
-                    Log.e(TAG, "Malformed device state posture pair: "
-                            + deviceStatePosturePair);
-                }
-                continue;
-            }
+        mDeviceStateMapper =
+                new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
 
-            int deviceState;
-            int posture;
-            try {
-                deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
-                posture = Integer.parseInt(deviceStatePostureMapping[1]);
-            } catch (NumberFormatException e) {
-                if (DEBUG) {
-                    Log.e(TAG, "Failed to parse device state or posture: "
-                                    + deviceStatePosturePair,
-                            e);
-                }
-                continue;
-            }
-            isHalfOpenedSupported = isHalfOpenedSupported
-                    || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
-            mDeviceStateToPostureMap.put(deviceState, posture);
-        }
-        mIsHalfOpenedSupported = isHalfOpenedSupported;
-        if (mDeviceStateToPostureMap.size() > 0) {
+        if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
+            final Executor executor =
+                    Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor();
             Objects.requireNonNull(deviceStateManager)
-                    .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+                    .registerCallback(executor, mDeviceStateCallback);
         }
     }
 
@@ -137,50 +126,51 @@
      * Add a callback to mCallbacks if there is no device state. This callback will be run
      * once a device state is set. Otherwise,run the callback immediately.
      */
-    private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
-            String displayFeaturesString) {
-        if (isCurrentStateValid()) {
-            callback.accept(calculateFoldingFeature(displayFeaturesString));
+    private void runCallbackWhenValidState(@NonNull DeviceState state,
+            @NonNull Consumer<List<CommonFoldingFeature>> callback,
+            @NonNull String displayFeaturesString) {
+        if (mDeviceStateMapper.isDeviceStateValid(state)) {
+            callback.accept(calculateFoldingFeature(state, displayFeaturesString));
         } else {
             // This callback will be added to mCallbacks and removed once it runs once.
-            AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+            final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
                     new AcceptOnceConsumer<>(this, callback);
             addDataChangedCallback(singleRunCallback);
         }
     }
 
-    /**
-     * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
-     * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
-     * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
-     * Returns a boolean value of whether the device state is valid.
-     */
-    private boolean isCurrentStateValid() {
-        // If the device state is not found in the map, indexOfKey returns a negative number.
-        return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
-    }
-
+    // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of
+    // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations.
+    @SuppressWarnings("GuardedBy")
     @Override
     protected void onListenersChanged() {
         super.onListenersChanged();
-        if (hasListeners()) {
-            mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
-        } else {
-            mCurrentDeviceState = new DeviceState(
-                    new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
-                            "INVALID").build());
-            mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+        synchronized (mCurrentDeviceStateLock) {
+            if (hasListeners()) {
+                mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+            } else {
+                mCurrentDeviceState = INVALID_DEVICE_STATE;
+                mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+            }
+        }
+    }
+
+    @NonNull
+    private DeviceState getCurrentDeviceState() {
+        synchronized (mCurrentDeviceStateLock) {
+            return mCurrentDeviceState;
         }
     }
 
     @NonNull
     @Override
     public Optional<List<CommonFoldingFeature>> getCurrentData() {
-        Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
-        if (!isCurrentStateValid()) {
+        final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+        final DeviceState state = getCurrentDeviceState();
+        if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) {
             return Optional.empty();
         } else {
-            return displayFeaturesString.map(this::calculateFoldingFeature);
+            return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get()));
         }
     }
 
@@ -191,7 +181,7 @@
      */
     @NonNull
     public List<CommonFoldingFeature> getFoldsWithUnknownState() {
-        Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
+        final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
 
         if (optionalFoldingFeatureString.isPresent()) {
             return CommonFoldingFeature.parseListFromString(
@@ -201,7 +191,6 @@
         return Collections.emptyList();
     }
 
-
     /**
      * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
      * {@link DeviceStateManagerFoldingFeatureProducer}.
@@ -218,16 +207,16 @@
         return foldFeatures;
     }
 
-
     /**
      * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
      */
     public boolean isHalfOpenedSupported() {
-        return mIsHalfOpenedSupported;
+        return mDeviceStateMapper.mIsHalfOpenedSupported;
     }
 
     /**
      * Adds the data to the storeFeaturesConsumer when the data is ready.
+     *
      * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
      */
     @Override
@@ -236,38 +225,123 @@
             if (TextUtils.isEmpty(displayFeaturesString)) {
                 storeFeaturesConsumer.accept(new ArrayList<>());
             } else {
-                runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+                final DeviceState state = getCurrentDeviceState();
+                runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString);
             }
         });
     }
 
-    private void notifyFoldingFeatureChange(String displayFeaturesString) {
-        if (!isCurrentStateValid()) {
+    @GuardedBy("mCurrentDeviceStateLock")
+    private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) {
+        final DeviceState state = mCurrentDeviceState;
+        if (!mDeviceStateMapper.isDeviceStateValid(state)) {
             return;
         }
         if (TextUtils.isEmpty(displayFeaturesString)) {
             notifyDataChanged(new ArrayList<>());
         } else {
-            notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
+            notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString));
         }
     }
 
-    private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
-        return parseListFromString(displayFeaturesString, currentHingeState());
-    }
-
-    @CommonFoldingFeature.State
-    private int currentHingeState() {
+    @NonNull
+    private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState,
+            @NonNull String displayFeaturesString) {
         @CommonFoldingFeature.State
-        int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
-                COMMON_STATE_UNKNOWN);
+        final int hingeState = mDeviceStateMapper.getHingeState(deviceState);
+        return parseListFromString(displayFeaturesString, hingeState);
+    }
 
-        if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
-            posture = mDeviceStateToPostureMap.get(
-                    DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
-                            mSupportedStates), COMMON_STATE_UNKNOWN);
+    /**
+     * Internal class to map device states to corresponding postures.
+     *
+     * <p>This class encapsulates the logic for mapping device states to postures. The mapping is
+     * immutable after initialization to ensure thread safety.
+     */
+    private static class DeviceStateMapper {
+        /**
+         * Emulated device state
+         * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
+         * {@link CommonFoldingFeature.State} map.
+         *
+         * <p>This map must be immutable after initialization to ensure thread safety, as it may be
+         * accessed from multiple threads. Modifications should only occur during object
+         * construction.
+         */
+        private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+        /**
+         * The list of device states that are supported.
+         *
+         * <p>This list must be immutable after initialization to ensure thread safety.
+         */
+        @NonNull
+        private final List<DeviceState> mSupportedStates;
+
+        final boolean mIsHalfOpenedSupported;
+
+        DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) {
+            mSupportedStates = supportedStates;
+
+            final String[] deviceStatePosturePairs = context.getResources()
+                    .getStringArray(R.array.config_device_state_postures);
+            boolean isHalfOpenedSupported = false;
+            for (String deviceStatePosturePair : deviceStatePosturePairs) {
+                final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+                if (deviceStatePostureMapping.length != 2) {
+                    if (DEBUG) {
+                        Log.e(TAG, "Malformed device state posture pair: "
+                                + deviceStatePosturePair);
+                    }
+                    continue;
+                }
+
+                final int deviceState;
+                final int posture;
+                try {
+                    deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+                    posture = Integer.parseInt(deviceStatePostureMapping[1]);
+                } catch (NumberFormatException e) {
+                    if (DEBUG) {
+                        Log.e(TAG, "Failed to parse device state or posture: "
+                                        + deviceStatePosturePair,
+                                e);
+                    }
+                    continue;
+                }
+                isHalfOpenedSupported = isHalfOpenedSupported
+                        || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+                mDeviceStateToPostureMap.put(deviceState, posture);
+            }
+            mIsHalfOpenedSupported = isHalfOpenedSupported;
         }
 
-        return posture;
+        boolean isDeviceStateToPostureMapEmpty() {
+            return mDeviceStateToPostureMap.size() == 0;
+        }
+
+        /**
+         * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap}
+         * which was initialized in the constructor of {@link DeviceStateMapper}.
+         * Returns a boolean value of whether the device state is valid.
+         */
+        boolean isDeviceStateValid(@NonNull DeviceState deviceState) {
+            // If the device state is not found in the map, indexOfKey returns a negative number.
+            return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0;
+        }
+
+        @CommonFoldingFeature.State
+        int getHingeState(@NonNull DeviceState deviceState) {
+            @CommonFoldingFeature.State
+            final int posture =
+                    mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN);
+            if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+                return posture;
+            }
+
+            final int baseStateIdentifier =
+                    DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates);
+            return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN);
+        }
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 74cce68..dcc2d93 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -377,8 +377,16 @@
 
     @Nullable
     TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
-        return getContainer(container -> container.hasAppearedActivity(activityToken)
-                || container.hasPendingAppearedActivity(activityToken));
+        // When the new activity is launched to the topmost TF because the source activity
+        // was in that TF, and the source activity is finished before resolving the new activity,
+        // we will try to see if the new activity match a rule with the split activities below.
+        // If matched, it can be reparented.
+        final TaskFragmentContainer taskFragmentContainer
+                = getContainer(container -> container.hasPendingAppearedActivity(activityToken));
+        if (taskFragmentContainer != null) {
+            return taskFragmentContainer;
+        }
+        return getContainer(container -> container.hasAppearedActivity(activityToken));
     }
 
     @Nullable
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index bd430c0..09185ee 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -29,6 +29,7 @@
 
     srcs: [
         "**/*.java",
+        "**/*.kt",
     ],
 
     static_libs: [
@@ -41,6 +42,7 @@
         "androidx.test.ext.junit",
         "flag-junit",
         "mockito-target-extended-minus-junit4",
+        "mockito-kotlin-nodeps",
         "truth",
         "testables",
         "platform-test-annotations",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
new file mode 100644
index 0000000..90887a7
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.hardware.devicestate.DeviceState
+import android.hardware.devicestate.DeviceStateManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.window.common.layout.CommonFoldingFeature
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE
+import androidx.window.common.layout.DisplayFoldFeatureCommon
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN
+import com.android.internal.R
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+/**
+ * Test class for [DeviceStateManagerFoldingFeatureProducer].
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
+ */
+@RunWith(AndroidJUnit4::class)
+class DeviceStateManagerFoldingFeatureProducerTest {
+    @get:Rule
+    val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    private val mMockDeviceStateManager = mock<DeviceStateManager>()
+    private val mMockResources = mock<Resources> {
+        on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES
+    }
+    private val mMockContext = mock<Context> {
+        on { resources } doReturn mMockResources
+    }
+    private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> {
+        on { currentData } doReturn Optional.of(DISPLAY_FEATURES)
+        on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+            val callback = invocation.getArgument(0) as Consumer<String>
+            callback.accept(DISPLAY_FEATURES)
+        }
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() {
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() {
+        val executorCaptor = ArgumentCaptor.forClass(Executor::class.java)
+        val runnable = mock<Runnable>()
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any())
+        executorCaptor.value.execute(runnable)
+        verify(runnable).run()
+    }
+
+    @Test
+    fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+
+        val currentData = ffp.getCurrentData()
+
+        assertThat(currentData).isPresent()
+        assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val currentData = ffp.getCurrentData()
+
+        assertThat(currentData).isEmpty()
+    }
+
+    @Test
+    fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.getFoldsWithUnknownState()
+
+        assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() {
+        mRawFoldSupplier.stub {
+            on { currentData } doReturn Optional.empty()
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.getFoldsWithUnknownState()
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() {
+        mRawFoldSupplier.stub {
+            on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE)
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        val result = ffp.displayFeatures
+
+        assertThat(result).containsExactly(
+            DisplayFoldFeatureCommon(
+                DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN,
+                setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED),
+            ),
+        )
+    }
+
+    @Test
+    fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        assertThat(ffp.isHalfOpenedSupported).isTrue()
+    }
+
+    @Test
+    fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() {
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray()
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        assertThat(ffp.isHalfOpenedSupported).isFalse()
+    }
+
+    @Test
+    fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() {
+        mRawFoldSupplier.stub {
+            on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+                val callback = invocation.getArgument(0) as Consumer<String>
+                callback.accept("")
+            }
+        }
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer).accept(emptyList())
+    }
+
+    @Test
+    fun testGetData_validState_callsConsumerWithFoldingFeatures() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() {
+        val ffp = DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+        ffp.getData(storeFeaturesConsumer)
+
+        verify(storeFeaturesConsumer, never()).accept(any())
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
+        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+    }
+
+    @Test
+    fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() {
+        val malformedDeviceStatePostures = arrayOf(
+            // Missing the posture.
+            "0",
+            // Empty string.
+            "",
+            // Too many elements.
+            "0:1:2",
+        )
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn
+                    malformedDeviceStatePostures
+        }
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+    }
+
+    @Test
+    fun testDeviceStateMapper_invalidNumberFormat_skipsPair() {
+        val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1")
+        mMockResources.stub {
+            on { getStringArray(R.array.config_device_state_postures) } doReturn
+                    invalidNumberFormatDeviceStatePostures
+        }
+
+        DeviceStateManagerFoldingFeatureProducer(
+            mMockContext,
+            mRawFoldSupplier,
+            mMockDeviceStateManager,
+        )
+
+        verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+    }
+
+    companion object {
+        // Supported device states configuration.
+        private enum class SupportedDeviceStates {
+            CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT;
+
+            override fun toString() = ordinal.toString()
+
+            fun toDeviceState(): DeviceState =
+                DeviceState(DeviceState.Configuration.Builder(ordinal, name).build())
+        }
+
+        // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture.
+        private val DEVICE_STATE_POSTURES =
+            arrayOf(
+                "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES",
+                "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED",
+                "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT",
+                "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES",
+                "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE",
+            )
+        private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState()
+        private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState()
+
+        // WindowsManager Jetpack display features.
+        private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]"
+        private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened"
+        private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+            DISPLAY_FEATURES,
+            COMMON_STATE_HALF_OPENED,
+        )
+        private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+            DISPLAY_FEATURES,
+            COMMON_STATE_UNKNOWN,
+        )
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 7fab371..bc4916a 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -535,7 +535,8 @@
         // container1.
         container2.setInfo(mTransaction, mInfo);
 
-        assertTrue(container2.hasActivity(mActivity.getActivityToken()));
+        assertTrue(container1.hasActivity(mActivity.getActivityToken()));
+        assertFalse(container2.hasActivity(mActivity.getActivityToken()));
         // When the pending appeared record is removed from container1, we respect the appeared
         // record in container2.
         container1.removePendingAppearedActivity(mActivity.getActivityToken());
diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
new file mode 100644
index 0000000..07e5ac1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?android:attr/textColorTertiary"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/system_on_tertiary_fixed"
+        android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
new file mode 100644
index 0000000..a12a746
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="30dp" />
+            <solid android:color="@android:color/system_tertiary_fixed" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
new file mode 100644
index 0000000..aadffb5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- An arrow that points towards left. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="10dp"
+    android:height="12dp"
+    android:viewportWidth="10"
+    android:viewportHeight="12">
+  <path
+      android:pathData="M2.858,4.285C1.564,5.062 1.564,6.938 2.858,7.715L10,12L10,0L2.858,4.285Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
new file mode 100644
index 0000000..e3c9a66
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- An arrow that points upwards. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="9dp"
+    android:viewportWidth="12"
+    android:viewportHeight="9">
+  <path
+      android:pathData="M7.715,1.858C6.938,0.564 5.062,0.564 4.285,1.858L0,9L12,9L7.715,1.858Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
new file mode 100644
index 0000000..a269b9e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="horizontal">
+
+    <!-- ImageView for the arrow icon, positioned horizontally at the start of the tooltip
+    container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="10dp"
+        android:layout_height="12dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
new file mode 100644
index 0000000..bdee883
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tooltip_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/desktop_windowing_education_tooltip_background"
+    android:orientation="horizontal"
+    android:padding="@dimen/desktop_windowing_education_tooltip_padding">
+
+    <ImageView
+        android:id="@+id/tooltip_icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/app_handle_education_tooltip_icon" />
+
+    <TextView
+        android:id="@+id/tooltip_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="2dp"
+        android:lineHeight="20dp"
+        android:maxWidth="150dp"
+        android:textColor="@android:color/system_on_tertiary_fixed"
+        android:textFontWeight="500"
+        android:textSize="14sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
new file mode 100644
index 0000000..c73c1da
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="vertical">
+
+    <!-- ImageView for the arrow icon, positioned vertically above the tooltip container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="12dp"
+        android:layout_height="9dp"
+        android:layout_gravity="center_horizontal"
+        android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
index 045b975..462a49c 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -99,11 +99,11 @@
 
                 </LinearLayout>
 
-                <FrameLayout
+
+                <LinearLayout
                     android:minHeight="@dimen/letterbox_restart_dialog_button_height"
-                    android:layout_width="match_parent"
+                    android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    style="?android:attr/buttonBarButtonStyle"
                     android:layout_gravity="end">
 
                     <Button
@@ -133,7 +133,7 @@
                         android:text="@string/letterbox_restart_restart"
                         android:contentDescription="@string/letterbox_restart_restart"/>
 
-                </FrameLayout>
+                </LinearLayout>
 
             </LinearLayout>
 
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3d87183..c7109f5 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -608,6 +608,9 @@
     <!-- The horizontal inset to apply to the close button's ripple drawable -->
     <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>
 
+    <!-- The padding added to all sides of windowing education tooltip -->
+    <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen>
+
     <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
     <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
     <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index bda5686..56f25da 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -219,6 +219,15 @@
          compatibility control. [CHAR LIMIT=NONE] -->
     <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string>
 
+    <!-- App handle education tooltip text for tooltip pointing to app handle -->
+    <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to windowing image button -->
+    <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to app chip -->
+    <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string>
+
     <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] -->
     <string name="letterbox_education_dialog_title">See and do more</string>
 
@@ -307,12 +316,11 @@
     <!-- Maximize menu snap buttons string. -->
     <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
     <!-- Snap resizing non-resizable string. -->
-    <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string>
+    <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string>
     <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string>
     <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
     <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string>
-
 </resources>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
new file mode 100644
index 0000000..26aae2d
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Listener to get focus-related transition callbacks.
+ */
+@ExternalThread
+public interface FocusTransitionListener {
+    /**
+     * Called when a transition changes the top, focused display.
+     */
+    void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
new file mode 100644
index 0000000..b91d5b6
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+/**
+ * Listener interface that to get focus-related transition callbacks.
+ */
+oneway interface IFocusTransitionListener {
+
+    /**
+     * Called when a transition changes the top, focused display.
+     */
+    void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 3256abf..02615a9 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -20,6 +20,7 @@
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
+import com.android.wm.shell.shared.IFocusTransitionListener;
 import com.android.wm.shell.shared.IHomeTransitionListener;
 
 /**
@@ -59,4 +60,9 @@
      */
     oneway void registerRemoteForTakeover(in TransitionFilter filter,
             in RemoteTransition remoteTransition) = 6;
+
+    /**
+     * Set listener that will receive callbacks about transitions involving focus switch.
+     */
+    oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7;
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index 6d4ab4c..2db4311 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -22,6 +22,8 @@
 
 import com.android.wm.shell.shared.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to manage remote transitions.
  */
@@ -44,4 +46,15 @@
      * Unregisters a remote transition for all operations.
      */
     default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
+
+    /**
+     * Sets listener that will receive callbacks about transitions involving focus switch.
+     */
+    default void setFocusTransitionListener(@NonNull FocusTransitionListener listener,
+            Executor executor) {}
+
+    /**
+     * Unsets listener that will receive callbacks about transitions involving focus switch.
+     */
+    default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index bec2ea5..4227a6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -123,6 +123,7 @@
 import com.android.wm.shell.taskview.TaskViewFactory;
 import com.android.wm.shell.taskview.TaskViewFactoryController;
 import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.HomeTransitionObserver;
 import com.android.wm.shell.transition.MixedTransitionHandler;
 import com.android.wm.shell.transition.Transitions;
@@ -742,14 +743,15 @@
             @ShellMainThread Handler mainHandler,
             @ShellAnimationThread ShellExecutor animExecutor,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            HomeTransitionObserver homeTransitionObserver) {
+            HomeTransitionObserver homeTransitionObserver,
+            FocusTransitionObserver focusTransitionObserver) {
         if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
             // TODO(b/238217847): Force override shell init if registration is disabled
             shellInit = new ShellInit(mainExecutor);
         }
         return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
                 pool, displayController, mainExecutor, mainHandler, animExecutor,
-                rootTaskDisplayAreaOrganizer, homeTransitionObserver);
+                rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver);
     }
 
     @WMSingleton
@@ -761,6 +763,12 @@
 
     @WMSingleton
     @Provides
+    static FocusTransitionObserver provideFocusTransitionObserver() {
+        return new FocusTransitionObserver();
+    }
+
+    @WMSingleton
+    @Provides
     static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) {
         return new TaskViewTransitions(transitions);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 759ed03..0e8c4e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -456,6 +456,7 @@
             pw.println(
                 "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}"
             )
+            pw.println("${innerPrefix}minimizedTasks=${data.minimizedTasks.toDumpString()}")
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 968f40c..afa27f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -43,6 +43,7 @@
 import android.view.DragEvent
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -1061,7 +1062,10 @@
                     // Check if freeform task launch during recents should be handled
                     shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
                     // Check if the closing task needs to be handled
-                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
+                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(
+                        task,
+                        request.type
+                    )
                     // Check if the top task shouldn't be allowed to enter desktop mode
                     isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task)
                     // Check if fullscreen task should be updated
@@ -1288,7 +1292,10 @@
     }
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
-    private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+    private fun handleTaskClosing(
+        task: RunningTaskInfo,
+        transitionType: Int
+    ): WindowContainerTransaction? {
         logV("handleTaskClosing")
         if (!isDesktopModeShowing(task.displayId))
             return null
@@ -1301,9 +1308,10 @@
             removeWallpaperActivity(wct)
         }
         taskRepository.addClosingTask(task.displayId, task.taskId)
-        // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
+        // If a CLOSE is triggered on a desktop task, remove the task.
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
-            taskRepository.isVisibleTask(task.taskId)
+            taskRepository.isVisibleTask(task.taskId) &&
+            transitionType == TRANSIT_CLOSE
         ) {
             wct.removeTask(task.token)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 0841628..4796c4d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -16,16 +16,19 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_TO_BACK
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
+import android.window.flags.DesktopModeFlags
+import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
-import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
@@ -64,6 +67,30 @@
     ) {
         // TODO: b/332682201 Update repository state
         updateWallpaperToken(info)
+
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+            handleBackNavigation(info)
+        }
+    }
+
+    private fun handleBackNavigation(info: TransitionInfo) {
+        // When default back navigation happens, transition type is TO_BACK and the change is
+        // TO_BACK. Mark the task going to back as minimized.
+        if (info.type == TRANSIT_TO_BACK) {
+            for (change in info.changes) {
+                val taskInfo = change.taskInfo
+                if (taskInfo == null || taskInfo.taskId == -1) {
+                    continue
+                }
+
+                if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+                    change.mode == TRANSIT_TO_BACK &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+                ) {
+                    desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+                }
+            }
+        }
     }
 
     override fun onTransitionStarting(transition: IBinder) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 2138acc..cbb08b8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -1344,6 +1344,9 @@
         final SurfaceControl leash = pipChange.getLeash();
         final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds();
         final boolean isInPip = mPipTransitionState.isInPip();
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b",
+                TAG, pipChange, destBounds, isInPip);
         mSurfaceTransactionHelper
                 .crop(startTransaction, leash, destBounds)
                 .round(startTransaction, leash, isInPip)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
new file mode 100644
index 0000000..2f5059f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
+import static com.android.wm.shell.transition.Transitions.TransitionObserver;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * The {@link TransitionObserver} that observes for transitions involving focus switch.
+ * It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
+ * and callers within the process via {@link FocusTransitionListener}.
+ */
+public class FocusTransitionObserver implements TransitionObserver {
+    private static final String TAG = FocusTransitionObserver.class.getSimpleName();
+
+    private IFocusTransitionListener mRemoteListener;
+    private final Map<FocusTransitionListener, Executor> mLocalListeners =
+            new HashMap<>();
+
+    private int mFocusedDisplayId = INVALID_DISPLAY;
+
+    public FocusTransitionObserver() {}
+
+    @Override
+    public void onTransitionReady(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        final List<TransitionInfo.Change> changes = info.getChanges();
+        for (int i = changes.size() - 1; i >= 0; i--) {
+            final TransitionInfo.Change change = changes.get(i);
+            final RunningTaskInfo task = change.getTaskInfo();
+            if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+                if (mFocusedDisplayId != task.displayId) {
+                    mFocusedDisplayId = task.displayId;
+                    notifyFocusedDisplayChanged();
+                }
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onTransitionStarting(@NonNull IBinder transition) {}
+
+    @Override
+    public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
+
+    @Override
+    public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the Shell, within the host process.
+     *
+     */
+    public void setLocalFocusTransitionListener(FocusTransitionListener listener,
+            Executor executor) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mLocalListeners.put(listener, executor);
+        executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId));
+    }
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the Shell, within the host process.
+     *
+     */
+    public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mLocalListeners.remove(listener);
+    }
+
+    /**
+     * Sets the focus transition listener that receives any transitions resulting in focus switch.
+     * This is for calls from outside the host process.
+     */
+    public void setRemoteFocusTransitionListener(Transitions transitions,
+            IFocusTransitionListener listener) {
+        if (!enableDisplayFocusInShellTransitions()) {
+            return;
+        }
+        mRemoteListener = listener;
+        notifyFocusedDisplayChangedToRemote();
+    }
+
+    /**
+     * Notifies the listener that display focus has changed.
+     */
+    public void notifyFocusedDisplayChanged() {
+        notifyFocusedDisplayChangedToRemote();
+        mLocalListeners.forEach((listener, executor) ->
+                executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)));
+    }
+
+    private void notifyFocusedDisplayChangedToRemote() {
+        if (mRemoteListener != null) {
+            try {
+                mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e);
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d03832d..d280dcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -87,6 +87,8 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
 import com.android.wm.shell.shared.IHomeTransitionListener;
 import com.android.wm.shell.shared.IShellTransitions;
 import com.android.wm.shell.shared.ShellTransitions;
@@ -103,6 +105,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.concurrent.Executor;
 
 /**
  * Plays transition animations. Within this player, each transition has a lifecycle.
@@ -224,6 +227,7 @@
     private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
 
     private HomeTransitionObserver mHomeTransitionObserver;
+    private FocusTransitionObserver mFocusTransitionObserver;
 
     /** List of {@link Runnable} instances to run when the last active transition has finished.  */
     private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
@@ -309,10 +313,12 @@
             @NonNull ShellExecutor mainExecutor,
             @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor,
-            @NonNull HomeTransitionObserver observer) {
+            @NonNull HomeTransitionObserver homeTransitionObserver,
+            @NonNull FocusTransitionObserver focusTransitionObserver) {
         this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
                 displayController, mainExecutor, mainHandler, animExecutor,
-                new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer);
+                new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
+                homeTransitionObserver, focusTransitionObserver);
     }
 
     public Transitions(@NonNull Context context,
@@ -326,7 +332,8 @@
             @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor,
             @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
-            @NonNull HomeTransitionObserver observer) {
+            @NonNull HomeTransitionObserver homeTransitionObserver,
+            @NonNull FocusTransitionObserver focusTransitionObserver) {
         mOrganizer = organizer;
         mContext = context;
         mMainExecutor = mainExecutor;
@@ -345,7 +352,8 @@
         mHandlers.add(mRemoteTransitionHandler);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
         shellInit.addInitCallback(this::onInit, this);
-        mHomeTransitionObserver = observer;
+        mHomeTransitionObserver = homeTransitionObserver;
+        mFocusTransitionObserver = focusTransitionObserver;
 
         if (android.tracing.Flags.perfettoTransitionTracing()) {
             mTransitionTracer = new PerfettoTransitionTracer();
@@ -384,6 +392,8 @@
 
         mShellCommandHandler.addCommandCallback("transitions", this, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
+
+        registerObserver(mFocusTransitionObserver);
     }
 
     public boolean isRegistered() {
@@ -1573,6 +1583,21 @@
             mMainExecutor.execute(
                     () -> mRemoteTransitionHandler.removeFiltered(remoteTransition));
         }
+
+        @Override
+        public void setFocusTransitionListener(FocusTransitionListener listener,
+                Executor executor) {
+            mMainExecutor.execute(() ->
+                    mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor));
+
+        }
+
+        @Override
+        public void unsetFocusTransitionListener(FocusTransitionListener listener) {
+            mMainExecutor.execute(() ->
+                    mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener));
+
+        }
     }
 
     /**
@@ -1634,6 +1659,15 @@
         }
 
         @Override
+        public void setFocusTransitionListener(IFocusTransitionListener listener) {
+            executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener",
+                    (transitions) -> {
+                        transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener(
+                                transitions, listener);
+                    });
+        }
+
+        @Override
         public SurfaceControl getHomeTaskOverlayContainer() {
             SurfaceControl[] result = new SurfaceControl[1];
             executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 226b0fb..1be26f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -107,4 +107,27 @@
         }
         windowManagerWrapper.updateViewLayout(view, lp)
     }
+
+    class Factory {
+        fun create(
+            windowManagerWrapper: WindowManagerWrapper,
+            taskId: Int,
+            x: Int,
+            y: Int,
+            width: Int,
+            height: Int,
+            flags: Int,
+            view: View,
+        ): AdditionalSystemViewContainer =
+            AdditionalSystemViewContainer(
+                windowManagerWrapper = windowManagerWrapper,
+                taskId = taskId,
+                x = x,
+                y = y,
+                width = width,
+                height = height,
+                flags = flags,
+                view = view
+            )
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
new file mode 100644
index 0000000..98413ee
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.education
+
+import android.annotation.DimenRes
+import android.annotation.LayoutRes
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.util.Size
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.WindowManager
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.R
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.windowdecor.WindowManagerWrapper
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+
+/**
+ * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that
+ * only one tooltip is displayed at a time.
+ */
+class DesktopWindowingEducationTooltipController(
+    private val context: Context,
+    private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
+) {
+  // TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme
+  private var tooltipView: View? = null
+  private var animator: PhysicsAnimator<View>? = null
+  private val springConfig by lazy {
+    PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+  }
+  private var popupWindow: AdditionalSystemViewContainer? = null
+
+  /**
+   * Shows education tooltip.
+   *
+   * @param tooltipViewConfig features of tooltip.
+   * @param taskId is used in the title of popup window created for the tooltip view.
+   */
+  fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) {
+    hideEducationTooltip()
+    tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
+    animator = createAnimator()
+    animateShowTooltipTransition()
+  }
+
+  /** Hide the current education view if visible */
+  private fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() }
+
+  /** Create education view by inflating layout provided. */
+  private fun createEducationTooltipView(
+      tooltipViewConfig: EducationViewConfig,
+      taskId: Int,
+  ): View {
+    val tooltipView =
+        LayoutInflater.from(context)
+            .inflate(
+                tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false)
+            .apply {
+              alpha = 0f
+              scaleX = 0f
+              scaleY = 0f
+
+              requireViewById<TextView>(R.id.tooltip_text).apply {
+                text = tooltipViewConfig.tooltipText
+              }
+
+              setOnTouchListener { _, motionEvent ->
+                if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) {
+                  hideEducationTooltip()
+                  tooltipViewConfig.onDismissAction()
+                  true
+                } else {
+                  false
+                }
+              }
+              setOnClickListener {
+                hideEducationTooltip()
+                tooltipViewConfig.onEducationClickAction()
+              }
+            }
+
+    val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection)
+    val tooltipViewGlobalCoordinates =
+        tooltipViewGlobalCoordinates(
+            tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates,
+            arrowDirection = tooltipViewConfig.arrowDirection,
+            tooltipDimen = tooltipDimens)
+    createTooltipPopupWindow(
+        taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView)
+
+    return tooltipView
+  }
+
+  /** Create animator for education transitions */
+  private fun createAnimator(): PhysicsAnimator<View>? =
+      tooltipView?.let {
+        PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
+      }
+
+  /** Animate show transition for the education view */
+  private fun animateShowTooltipTransition() {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 1f)
+        ?.spring(DynamicAnimation.SCALE_X, 1f)
+        ?.spring(DynamicAnimation.SCALE_Y, 1f)
+        ?.start()
+  }
+
+  /** Animate hide transition for the education view */
+  private fun animateHideTooltipTransition(endActions: () -> Unit) {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 0f)
+        ?.spring(DynamicAnimation.SCALE_X, 0f)
+        ?.spring(DynamicAnimation.SCALE_Y, 0f)
+        ?.start()
+    endActions()
+  }
+
+  /** Remove education tooltip and clean up all relative properties */
+  private fun cleanUp() {
+    tooltipView = null
+    animator = null
+    popupWindow?.releaseView()
+    popupWindow = null
+  }
+
+  private fun createTooltipPopupWindow(
+      taskId: Int,
+      tooltipViewGlobalCoordinates: Point,
+      tooltipDimen: Size,
+      tooltipView: View,
+  ) {
+    popupWindow =
+        additionalSystemViewContainerFactory.create(
+            windowManagerWrapper =
+                WindowManagerWrapper(context.getSystemService(WindowManager::class.java)),
+            taskId = taskId,
+            x = tooltipViewGlobalCoordinates.x,
+            y = tooltipViewGlobalCoordinates.y,
+            width = tooltipDimen.width,
+            height = tooltipDimen.height,
+            flags =
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+            view = tooltipView)
+  }
+
+  private fun tooltipViewGlobalCoordinates(
+      tooltipViewGlobalCoordinates: Point,
+      arrowDirection: TooltipArrowDirection,
+      tooltipDimen: Size,
+  ): Point {
+    var tooltipX = tooltipViewGlobalCoordinates.x
+    var tooltipY = tooltipViewGlobalCoordinates.y
+
+    // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow.
+    // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of
+    // the window to be created. Hence we will need to move the coordinates left/up in order
+    // to position the tooltip correctly.
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement
+      // half of tooltip width from [tooltipX] to horizontally position the tooltip.
+      tooltipX -= tooltipDimen.width / 2
+    } else {
+      // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement
+      // half of tooltip height from [tooltipY] to vertically position the tooltip.
+      tooltipY -= tooltipDimen.height / 2
+    }
+    return Point(tooltipX, tooltipY)
+  }
+
+  private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size {
+    val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container)
+    val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon)
+    tooltipBackground.measure(
+        /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+    arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+
+    var desiredWidth =
+        tooltipBackground.measuredWidth +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    var desiredHeight =
+        tooltipBackground.measuredHeight +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // desiredHeight currently does not account for the height of arrow, hence adding it.
+      desiredHeight += arrowView.height
+    } else {
+      // desiredWidth currently does not account for the width of arrow, hence adding it.
+      desiredWidth += arrowView.width
+    }
+
+    return Size(desiredWidth, desiredHeight)
+  }
+
+  private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
+    if (resourceId == Resources.ID_NULL) return 0
+    return context.resources.getDimensionPixelSize(resourceId)
+  }
+
+  /**
+   * The configuration for education view features:
+   *
+   * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip.
+   * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip
+   *   arrow.
+   * @property tooltipText Text to be added to the TextView of tooltip.
+   * @property arrowDirection Direction of arrow of the tooltip.
+   * @property onEducationClickAction Lambda to be executed when the tooltip is clicked.
+   * @property onDismissAction Lambda to be executed when the tooltip is dismissed.
+   */
+  data class EducationViewConfig(
+      @LayoutRes val tooltipViewLayout: Int,
+      val tooltipViewGlobalCoordinates: Point,
+      val tooltipText: String,
+      val arrowDirection: TooltipArrowDirection,
+      val onEducationClickAction: () -> Unit,
+      val onDismissAction: () -> Unit,
+  )
+
+  /** Direction of arrow of the tooltip */
+  enum class TooltipArrowDirection {
+    UP,
+    LEFT,
+  }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ee54520..29aea00 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
@@ -2086,16 +2086,13 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
-    assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token)
+    assertNull(result, "Should not handle request")
   }
 
   @Test
@@ -2137,26 +2134,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() {
+  fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -2183,23 +2162,7 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
   fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
@@ -2226,29 +2189,11 @@
 
     // Should create remove wallpaper transaction
     assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
   @EnableFlags(
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
   fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2261,23 +2206,6 @@
 
     // Should create remove wallpaper transaction
     assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
new file mode 100644
index 0000000..c989d16
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class DesktopTasksTransitionObserverTest {
+
+    @JvmField
+    @Rule
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this)
+            .mockStatic(DesktopModeStatus::class.java)
+            .build()!!
+
+    private val testExecutor = mock<ShellExecutor>()
+    private val mockShellInit = mock<ShellInit>()
+    private val transitions = mock<Transitions>()
+    private val context = mock<Context>()
+    private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+    private val taskRepository = mock<DesktopModeTaskRepository>()
+
+    private lateinit var transitionObserver: DesktopTasksTransitionObserver
+    private lateinit var shellInit: ShellInit
+
+    @Before
+    fun setup() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+        shellInit = spy(ShellInit(testExecutor))
+
+        transitionObserver =
+            DesktopTasksTransitionObserver(
+                context, taskRepository, transitions, shellTaskOrganizer, shellInit
+            )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun backNavigation_taskMinimized() {
+        val task = createTaskInfo(1)
+        whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info =
+            createBackNavigationTransition(task),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).minimizeTask(task.displayId, task.taskId)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun backNavigation_nullTaskInfo_taskNotMinimized() {
+        val task = createTaskInfo(1)
+        whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info =
+            createBackNavigationTransition(null),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
+    }
+
+    private fun createBackNavigationTransition(
+        task: RunningTaskInfo?
+    ): TransitionInfo {
+        return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+            addChange(
+                Change(mock(), mock()).apply {
+                    mode = TRANSIT_TO_BACK
+                    parent = null
+                    taskInfo = task
+                    flags = flags
+                }
+            )
+        }
+    }
+
+    private fun createTaskInfo(id: Int) =
+        RunningTaskInfo().apply {
+            taskId = id
+            displayId = DEFAULT_DISPLAY
+            configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java))
+            baseIntent = Intent().apply {
+                component = ComponentName("package", "component.name")
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index a6c16c4..67eda8b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -74,6 +74,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.HomeTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
 
@@ -429,7 +430,8 @@
         ShellInit shellInit = new ShellInit(mMainExecutor);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         return t;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
new file mode 100644
index 0000000..d37b4cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.TransitionMode;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+import com.android.wm.shell.shared.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for the focus transition observer.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS)
+public class FocusTransitionObserverTest extends ShellTestCase {
+
+    static final int SECONDARY_DISPLAY_ID = 1;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    private IFocusTransitionListener mListener;
+    private Transitions mTransition;
+    private FocusTransitionObserver mFocusTransitionObserver;
+
+    @Before
+    public void setUp() {
+        mListener = mock(IFocusTransitionListener.class);
+        when(mListener.asBinder()).thenReturn(mock(IBinder.class));
+
+        mFocusTransitionObserver = new FocusTransitionObserver();
+        mTransition =
+                new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        mock(ShellInit.class), mock(ShellController.class),
+                        mock(ShellTaskOrganizer.class), mock(TransactionPool.class),
+                        mock(DisplayController.class), new TestShellExecutor(),
+                        new Handler(Looper.getMainLooper()), new TestShellExecutor(),
+                        mock(HomeTransitionObserver.class),
+                mFocusTransitionObserver);
+        mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener);
+    }
+
+    @Test
+    public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+        final IBinder binder = mock(IBinder.class);
+        final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
+
+        // Open a task on the default display, which doesn't change display focus because the
+        // default display already has it.
+        TransitionInfo info = mock(TransitionInfo.class);
+        final List<TransitionInfo.Change> changes = new ArrayList<>();
+        setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+        clearInvocations(mListener);
+
+        // Open a new task on the secondary display and verify display focus changes to the display.
+        changes.clear();
+        setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+        clearInvocations(mListener);
+
+        // Open the first task to front and verify display focus goes back to the default display.
+        changes.clear();
+        setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+        clearInvocations(mListener);
+
+        // Open another task on the default display and verify no display focus switch as it's
+        // already on the default display.
+        changes.clear();
+        setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+                true /* focused */);
+        when(info.getChanges()).thenReturn(changes);
+        mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+        verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+    }
+
+    private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+            @TransitionMode int mode, int displayId, boolean focused) {
+        TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+        RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
+        taskInfo.taskId = taskId;
+        taskInfo.isFocused = focused;
+        when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused);
+        taskInfo.displayId = displayId;
+        when(change.getTaskInfo()).thenReturn(taskInfo);
+        when(change.getMode()).thenReturn(mode);
+        changes.add(change);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 8f49de0..8dfdfb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -100,7 +100,8 @@
         mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor);
         mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class),
                 mOrganizer, mTransactionPool, mDisplayController, mMainExecutor,
-                mMainHandler, mAnimExecutor, mHomeTransitionObserver);
+                mMainHandler, mAnimExecutor, mHomeTransitionObserver,
+                mock(FocusTransitionObserver.class));
         mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index aea14b9..6cde056 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -158,7 +158,8 @@
         ShellInit shellInit = mock(ShellInit.class);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         // One from Transitions, one from RootTaskDisplayAreaOrganizer
         verify(shellInit).addInitCallback(any(), eq(t));
         verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class));
@@ -170,7 +171,8 @@
         ShellController shellController = mock(ShellController.class);
         final Transitions t = new Transitions(mContext, shellInit, shellController,
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         verify(shellController, times(1)).addExternalInterface(
                 eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any());
@@ -1238,7 +1240,8 @@
         final Transitions transitions =
                 new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
                         mTransactionPool, createTestDisplayController(), mMainExecutor,
-                        mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                        mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                        mock(FocusTransitionObserver.class));
         final RecentsTransitionHandler recentsHandler =
                 new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
                         mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
@@ -1780,7 +1783,8 @@
         ShellInit shellInit = new ShellInit(mMainExecutor);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
                 mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
-                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+                mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+                mock(FocusTransitionObserver.class));
         shellInit.init();
         return t;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
new file mode 100644
index 0000000..5594981
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.education
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.graphics.Point
+import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() {
+  @Mock private lateinit var mockWindowManager: WindowManager
+  @Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory
+  private lateinit var testableResources: TestableResources
+  private lateinit var testableContext: TestableContext
+  private lateinit var tooltipController: DesktopWindowingEducationTooltipController
+  private val tooltipViewArgumentCaptor = argumentCaptor<View>()
+
+  @Before
+  fun setUp() {
+    MockitoAnnotations.initMocks(this)
+    testableContext = TestableContext(mContext)
+    testableResources =
+        testableContext.orCreateTestableResources.apply {
+          addOverride(R.dimen.desktop_windowing_education_tooltip_padding, 10)
+        }
+    testableContext.addMockSystemService(
+        Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+    testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager)
+    tooltipController =
+        DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory)
+  }
+
+  @Test
+  fun showEducationTooltip_createsTooltipWithCorrectText() {
+    val tooltipText = "This is a tooltip"
+    val tooltipViewConfig = createTooltipConfig(tooltipText = tooltipText)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val tooltipTextView =
+        tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text)
+    assertThat(tooltipTextView.text).isEqualTo(tooltipText)
+  }
+
+  @Test
+  fun showEducationTooltip_usesCorrectTaskIdForWindow() {
+    val tooltipViewConfig = createTooltipConfig()
+    val taskIdArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = taskIdArgumentCaptor.capture(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = anyOrNull())
+    assertThat(taskIdArgumentCaptor.lastValue).isEqualTo(123)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsUpwards_horizontallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.UP,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipXArgumentCaptor = argumentCaptor<Int>()
+    val tooltipWidthArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = tooltipXArgumentCaptor.capture(),
+            y = anyInt(),
+            width = tooltipWidthArgumentCaptor.capture(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipX = initialTooltipX - tooltipWidthArgumentCaptor.lastValue / 2
+    assertThat(tooltipXArgumentCaptor.lastValue).isEqualTo(expectedTooltipX)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsLeft_verticallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.LEFT,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipYArgumentCaptor = argumentCaptor<Int>()
+    val tooltipHeightArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = tooltipYArgumentCaptor.capture(),
+            width = anyInt(),
+            height = tooltipHeightArgumentCaptor.capture(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipY = initialTooltipY - tooltipHeightArgumentCaptor.lastValue / 2
+    assertThat(tooltipYArgumentCaptor.lastValue).isEqualTo(expectedTooltipY)
+  }
+
+  @Test
+  fun showEducationTooltip_touchEventActionOutside_dismissActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onDismissAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val motionEvent =
+        MotionEvent.obtain(
+            /* downTime= */ 0L,
+            /* eventTime= */ 0L,
+            MotionEvent.ACTION_OUTSIDE,
+            /* x= */ 0f,
+            /* y= */ 0f,
+            /* metaState= */ 0)
+    tooltipViewArgumentCaptor.lastValue.dispatchTouchEvent(motionEvent)
+
+    verify(mockLambda).invoke()
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipClicked_onClickActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onEducationClickAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    tooltipViewArgumentCaptor.lastValue.performClick()
+
+    verify(mockLambda).invoke()
+  }
+
+  private fun createTooltipConfig(
+      @LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip,
+      tooltipViewGlobalCoordinates: Point = Point(0, 0),
+      tooltipText: String = "This is a tooltip",
+      arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP,
+      onEducationClickAction: () -> Unit = {},
+      onDismissAction: () -> Unit = {}
+  ) =
+      DesktopWindowingEducationTooltipController.EducationViewConfig(
+          tooltipViewLayout = tooltipViewLayout,
+          tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates,
+          tooltipText = tooltipText,
+          arrowDirection = arrowDirection,
+          onEducationClickAction = onEducationClickAction,
+          onDismissAction = onDismissAction,
+      )
+}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index 504e329..3ed33db 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -3,13 +3,15 @@
 
   public final class AppFunctionManager {
     ctor public AppFunctionManager(android.content.Context);
-    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
   }
 
   public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
     field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index b1dd467..815fe05 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -19,12 +19,12 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.CancellationSignal;
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
-
 /**
  * Provides app functions related functionalities.
  *
@@ -45,7 +45,7 @@
      *
      * @param context A {@link Context}.
      * @throws java.lang.IllegalStateException if the underlying {@link
-     *   android.app.appfunctions.AppFunctionManager} is not found.
+     *     android.app.appfunctions.AppFunctionManager} is not found.
      */
     public AppFunctionManager(Context context) {
         mContext = Objects.requireNonNull(context);
@@ -66,6 +66,7 @@
     public void executeAppFunction(
             @NonNull ExecuteAppFunctionRequest sidecarRequest,
             @NonNull @CallbackExecutor Executor executor,
+            @NonNull CancellationSignal cancellationSignal,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
         Objects.requireNonNull(sidecarRequest);
         Objects.requireNonNull(executor);
@@ -74,9 +75,40 @@
         android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
                 SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
         mManager.executeAppFunction(
-                platformRequest, executor, (platformResponse) -> {
-                    callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
-                            platformResponse));
+                platformRequest,
+                executor,
+                cancellationSignal,
+                (platformResponse) -> {
+                    callback.accept(
+                            SidecarConverter.getSidecarExecuteAppFunctionResponse(
+                                    platformResponse));
                 });
     }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Proxies request and response to the underlying {@link
+     * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+     * response in the appropriate type required by the function.
+     *
+     * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+     *     CancellationSignal, Consumer)} instead. This method will be removed once usage references
+     *     are updated.
+     */
+    @Deprecated
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest sidecarRequest,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        Objects.requireNonNull(sidecarRequest);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        executeAppFunction(
+                sidecarRequest,
+                executor,
+                new CancellationSignal(),
+                callback);
+    }
 }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 65959df..6023c97 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -25,6 +25,7 @@
 import android.content.Intent;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.CancellationSignal;
 
 import java.util.function.Consumer;
 
@@ -69,10 +70,11 @@
     private final Binder mBinder =
             android.app.appfunctions.AppFunctionService.createBinder(
                     /* context= */ this,
-                    /* onExecuteFunction= */ (platformRequest, callback) -> {
+                    /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> {
                         AppFunctionService.this.onExecuteFunction(
                                 SidecarConverter.getSidecarExecuteAppFunctionRequest(
                                         platformRequest),
+                                cancellationSignal,
                                 (sidecarResponse) -> {
                                     callback.accept(
                                             SidecarConverter.getPlatformExecuteAppFunctionResponse(
@@ -105,9 +107,42 @@
      * result using the callback, no matter if the execution was successful or not.
      *
      * @param request The function execution request.
+     * @param cancellationSignal A {@link CancellationSignal} to cancel the request.
      * @param callback A callback to report back the result.
      */
     @MainThread
+    public void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        onExecuteFunction(request, callback);
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     *
+     * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+     *     Consumer)} instead. This method will be removed once usage references are updated.
+     */
+    @MainThread
+    @Deprecated
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull Consumer<ExecuteAppFunctionResponse> callback);
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
index fc63c0f..3e73ebd 100644
--- a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
+++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
@@ -35,11 +35,13 @@
             style="@style/SettingsLibActionButton.Expressive"
             android:layout_width="@dimen/settingslib_expressive_space_large3"
             android:layout_height="@dimen/settingslib_expressive_space_medium5"
-            android:layout_gravity="center_horizontal" />
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/text1"
             style="@style/SettingsLibActionButton.Expressive.Label"
-            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
 
     </LinearLayout>
 
@@ -55,11 +57,13 @@
             style="@style/SettingsLibActionButton.Expressive"
             android:layout_width="@dimen/settingslib_expressive_space_large3"
             android:layout_height="@dimen/settingslib_expressive_space_medium5"
-            android:layout_gravity="center_horizontal" />
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/text2"
             style="@style/SettingsLibActionButton.Expressive.Label"
-            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
 
     </LinearLayout>
 
@@ -75,11 +79,13 @@
             style="@style/SettingsLibActionButton.Expressive"
             android:layout_width="@dimen/settingslib_expressive_space_large3"
             android:layout_height="@dimen/settingslib_expressive_space_medium5"
-            android:layout_gravity="center_horizontal" />
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/text3"
             style="@style/SettingsLibActionButton.Expressive.Label"
-            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
 
     </LinearLayout>
 
@@ -95,10 +101,12 @@
             style="@style/SettingsLibActionButton.Expressive"
             android:layout_width="@dimen/settingslib_expressive_space_large3"
             android:layout_height="@dimen/settingslib_expressive_space_medium5"
-            android:layout_gravity="center_horizontal" />
+            android:layout_gravity="center_horizontal"
+            android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/text4"
             style="@style/SettingsLibActionButton.Expressive.Label"
-            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+            android:importantForAccessibility="no"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
index f011039..b286182 100644
--- a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
+++ b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
@@ -548,16 +548,18 @@
                 if (mButton instanceof MaterialButton) {
                     ((MaterialButton) mButton).setIcon(mIcon);
                 }
+                mButton.setEnabled(mIsEnabled);
+                mActionLayout.setOnClickListener(mListener);
+                mActionLayout.setEnabled(mIsEnabled);
+                mActionLayout.setContentDescription(mText);
             } else {
                 mButton.setText(mText);
                 mButton.setCompoundDrawablesWithIntrinsicBounds(
                         null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+                mButton.setOnClickListener(mListener);
+                mButton.setEnabled(mIsEnabled);
             }
 
-            mButton.setOnClickListener(mListener);
-            mButton.setEnabled(mIsEnabled);
-
-
             if (shouldBeVisible()) {
                 mButton.setVisibility(View.VISIBLE);
                 if (mIsExpressive) {
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index d4851e1..af07686 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -33,6 +33,7 @@
         "SettingsLibBarChartPreference",
         "SettingsLibButtonPreference",
         "SettingsLibBulletPreference",
+        "SettingsLibCardPreference",
         "SettingsLibCollapsingToolbarBaseActivity",
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibDisplayUtils",
diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp
new file mode 100644
index 0000000..1d871d1
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SettingsLibCardPreference",
+    use_resource_processor: true,
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+    ],
+}
diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml
new file mode 100644
index 0000000..717f66e
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget.preference.card">
+
+    <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
new file mode 100644
index 0000000..716ed41
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<com.google.android.material.card.MaterialCardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/SettingsLibCardStyle">
+
+    <LinearLayout
+        android:id="@+id/card_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:baselineAligned="false"
+        android:minHeight="@dimen/settingslib_expressive_space_large3"
+        android:paddingStart="@dimen/settingslib_expressive_space_small1"
+        android:paddingEnd="@dimen/settingslib_expressive_space_small1"
+        android:orientation="horizontal"
+        android:gravity="center_vertical">
+
+        <LinearLayout
+            android:id="@+id/icon_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="@dimen/settingslib_expressive_space_medium3"
+            android:minHeight="@dimen/settingslib_expressive_space_medium3"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@android:id/icon"
+                android:src="@drawable/settingslib_arrow_drop_down"
+                android:layout_width="@dimen/settingslib_expressive_space_medium3"
+                android:layout_height="@dimen/settingslib_expressive_space_medium3"
+                android:scaleType="centerInside"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/text_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+            android:paddingVertical="@dimen/settingslib_expressive_space_small2"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@android:id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:hyphenationFrequency="normalFast"
+                android:lineBreakWordStyle="phrase"
+                android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/>
+
+            <TextView
+                android:id="@android:id/summary"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:hyphenationFrequency="normalFast"
+                android:lineBreakWordStyle="phrase"
+                android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/>
+
+        </LinearLayout>
+
+        <ImageView
+            android:id="@android:id/closeButton"
+            android:layout_width="@dimen/settingslib_expressive_space_medium4"
+            android:layout_height="@dimen/settingslib_expressive_space_medium4"
+            android:padding="@dimen/settingslib_expressive_space_extrasmall4"
+            android:layout_gravity="center"
+            android:src="@drawable/settingslib_expressive_icon_close"
+            android:background="?android:attr/selectableItemBackground" />
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
new file mode 100644
index 0000000..4cbdea5
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<resources>
+    <style name="TextAppearance.CardTitle.SettingsLib"
+        parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
+        <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+        <item name="android:textSize">20sp</item>
+    </style>
+
+    <style name="TextAppearance.CardSummary.SettingsLib"
+        parent="@style/TextAppearance.PreferenceSummary.SettingsLib">
+        <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item>
+        <item name="android:textSize">14sp</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
new file mode 100644
index 0000000..eb14746
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.card.R
+
+/**
+ * The CardPreference shows a card like suggestion in homepage, which also support dismiss.
+ */
+class CardPreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    init {
+        layoutResource = R.layout.settingslib_expressive_preference_card
+    }
+    private var dismissible = false
+        set(value) {
+            if (field != value) {
+                field = value
+                notifyChanged()
+            }
+        }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        holder.isDividerAllowedBelow = false
+        holder.isDividerAllowedAbove = false
+
+        holder.findViewById(android.R.id.closeButton)?.let { dismissButton ->
+            dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE
+            dismissButton.setOnClickListener {
+                isVisible = false
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml
new file mode 100644
index 0000000..161ece7
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <size android:width="24dp" android:height="24dp"/>
+            <solid android:color="@color/settingslib_materialColorSurfaceDim"/>
+        </shape>
+    </item>
+    <item>
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportWidth="960"
+            android:viewportHeight="960">
+            <path
+                android:fillColor="@color/settingslib_materialColorOnSurface"
+                android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/>
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml
new file mode 100644
index 0000000..1b5d518
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <size android:width="24dp" android:height="24dp"/>
+            <solid android:color="@color/settingslib_materialColorSurfaceDim"/>
+        </shape>
+    </item>
+    <item>
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportWidth="960"
+            android:viewportHeight="960">
+            <path
+                android:fillColor="@color/settingslib_materialColorOnSurface"
+                android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml
new file mode 100644
index 0000000..245d368
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="@dimen/settingslib_expressive_space_small1"
+    android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4"
+    android:orientation="vertical"
+    android:animateLayoutChanges="true"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false">
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:textAlignment="viewStart"
+        android:clickable="false"
+        android:longClickable="false"
+        android:maxLines="10"
+        android:ellipsize="end"
+        android:textAppearance="@style/TextAppearance.TopIntroText"/>
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/collapse_button"
+        app:layout_constraintTop_toBottomOf="@android:id/title"
+        app:layout_constraintStart_toStartOf="parent"
+        android:text="@string/settingslib_expressive_text_expand"
+        app:icon="@drawable/settingslib_expressive_icon_expand"
+        style="@style/SettingslibTextButtonStyle.Expressive"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml
new file mode 100644
index 0000000..857dd79
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <declare-styleable name="CollapsableTextView">
+        <attr name="android:gravity"/>
+        <!-- The minimum number of lines when the textView collapsed. -->
+        <attr name="android:minLines"/>
+        <!-- Specifies that the textView is collapsable. -->
+        <attr name="isCollapsable" format="boolean"/>
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml
new file mode 100644
index 0000000..2273406
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- text of button to indicate user the textView is expandable [CHAR LIMIT=NONE] -->
+    <string name="settingslib_expressive_text_expand">Expand</string>
+    <!-- text of button to indicate user the textView is collapsable [CHAR LIMIT=NONE] -->
+    <string name="settingslib_expressive_text_collapse">Collapse</string>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 816433c..250c27e 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -170,6 +170,52 @@
         <item name="thumbIcon">@drawable/settingslib_expressive_switch_thumb_icon</item>
     </style>
 
+    <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive">
+        <item name="android:layout_gravity">center</item>
+        <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
+    </style>
+
+    <style name="SettingsLibCardStyle" parent="">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item>
+        <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item>
+        <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item>
+        <item name="cardElevation">0dp</item>
+        <item name="rippleColor">?android:attr/colorControlHighlight</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled"
+        parent="@style/Widget.Material3.Button">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:backgroundTint">@color/settingslib_materialColorPrimary</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconTint">@color/settingslib_materialColorOnPrimary</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Filled.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Filled.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
     <style name="SettingsLibButtonStyle.Expressive.Tonal"
         parent="@style/Widget.Material3.Button.TonalButton">
         <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
@@ -189,8 +235,59 @@
         <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
     </style>
 
-    <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive">
-        <item name="android:layout_gravity">center</item>
-        <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
+    <style name="SettingsLibButtonStyle.Expressive.Tonal.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Tonal.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Tonal.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline"
+        parent="@style/Widget.Material3.Button.OutlinedButton.Icon">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorPrimary</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconTint">@color/settingslib_materialColorPrimary</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+        <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="strokeColor">@color/settingslib_materialColorOutlineVariant</item>
+
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline.Large">
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Outline.Extra"
+        parent="@style/SettingsLibButtonStyle.Expressive.Outline.Large">
+        <item name="android:layout_width">match_parent</item>
+    </style>
+
+    <style name="SettingslibTextButtonStyle.Expressive"
+        parent="@style/Widget.Material3.Button.TextButton.Icon">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSurface</item>
+        <item name="iconTint">@null</item>
+        <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item>
+        <item name="rippleColor">?android:attr/colorControlHighlight</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
new file mode 100644
index 0000000..127f21a
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.settingslib.widget.theme.R
+import com.google.android.material.button.MaterialButton
+
+class CollapsableTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    private var isCollapsable: Boolean = false
+    private var isCollapsed: Boolean = false
+    private var minLines: Int = DEFAULT_MIN_LINES
+
+    private val titleTextView: TextView
+    private val collapseButton: MaterialButton
+    private val collapseButtonResources: CollapseButtonResources
+
+    init {
+        LayoutInflater.from(context)
+            .inflate(R.layout.settingslib_expressive_collapsable_textview, this)
+        titleTextView = findViewById(android.R.id.title)
+        collapseButton = findViewById(R.id.collapse_button)
+
+        collapseButtonResources = CollapseButtonResources(
+            context.getDrawable(R.drawable.settingslib_expressive_icon_collapse)!!,
+            context.getDrawable(R.drawable.settingslib_expressive_icon_expand)!!,
+            context.getString(R.string.settingslib_expressive_text_collapse),
+            context.getString(R.string.settingslib_expressive_text_expand)
+        )
+
+        collapseButton.setOnClickListener {
+            isCollapsed = !isCollapsed
+            updateView()
+        }
+
+        initAttributes(context, attrs, defStyleAttr)
+    }
+
+    private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
+        context.obtainStyledAttributes(
+            attrs, Attrs, defStyleAttr, 0
+        ).apply {
+            val gravity = getInt(GravityAttr, Gravity.START)
+            when (gravity) {
+                Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> {
+                    centerHorizontally(titleTextView)
+                    centerHorizontally(collapseButton)
+                }
+            }
+            recycle()
+        }
+    }
+
+    private fun centerHorizontally(view: View) {
+        (view.layoutParams as LayoutParams).apply {
+            startToStart = LayoutParams.PARENT_ID
+            endToEnd = LayoutParams.PARENT_ID
+            horizontalBias = 0.5f
+        }
+    }
+
+    /**
+     * Sets the text content of the CollapsableTextView.
+     * @param text The text to display.
+     */
+    fun setText(text: String) {
+        titleTextView.text = text
+    }
+
+    /**
+     * Sets whether the text view is collapsable.
+     * @param collapsable True if the text view should be collapsable, false otherwise.
+     */
+    fun setCollapsable(collapsable: Boolean) {
+        isCollapsable = collapsable
+        updateView()
+    }
+
+    /**
+     * Sets the minimum number of lines to display when collapsed.
+     * @param lines The minimum number of lines.
+     */
+    fun setMinLines(line: Int) {
+        minLines = line.coerceIn(1, DEFAULT_MAX_LINES)
+        updateView()
+    }
+
+    private fun updateView() {
+        when {
+            isCollapsed -> {
+                collapseButton.apply {
+                    text = collapseButtonResources.expandText
+                    icon = collapseButtonResources.expandIcon
+                }
+                titleTextView.maxLines = minLines
+            }
+
+            else -> {
+                collapseButton.apply {
+                    text = collapseButtonResources.collapseText
+                    icon = collapseButtonResources.collapseIcon
+                }
+                titleTextView.maxLines = DEFAULT_MAX_LINES
+            }
+        }
+        collapseButton.visibility = if (isCollapsable) VISIBLE else GONE
+    }
+
+    private data class CollapseButtonResources(
+        val collapseIcon: Drawable,
+        val expandIcon: Drawable,
+        val collapseText: String,
+        val expandText: String
+    )
+
+    companion object {
+        private const val DEFAULT_MAX_LINES = 10
+        private const val DEFAULT_MIN_LINES = 2
+
+        private val Attrs = R.styleable.CollapsableTextView
+        private val GravityAttr = R.styleable.CollapsableTextView_android_gravity
+    }
+}
+
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 460bf99..965c971 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -162,3 +162,6 @@
 /** Creates a new [TextStyle] which font weight set to medium. */
 internal fun TextStyle.toMediumWeight() =
     copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em)
+
+internal fun TextStyle.toSemiBoldWeight() =
+    copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 7e1df16..5bb57b8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -110,7 +110,7 @@
                 shape = RectangleShape,
                 colors = ButtonDefaults.filledTonalButtonColors(
                     containerColor = MaterialTheme.colorScheme.primaryContainer,
-                    contentColor = MaterialTheme.colorScheme.onPrimary,
+                    contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
                     disabledContainerColor = MaterialTheme.colorScheme.surface,
                 ),
                 contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 94d2c21..f99d206 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -41,9 +41,7 @@
 internal fun NavigateBack() {
     val navController = LocalNavController.current
     val contentDescription = stringResource(R.string.abc_action_bar_up_description)
-    BackAction(contentDescription) {
-        navController.navigateBack()
-    }
+    BackAction(contentDescription) { navController.navigateBack() }
 }
 
 /** Action that collapses the search bar. */
@@ -55,15 +53,35 @@
 
 @Composable
 private fun BackAction(contentDescription: String, onClick: () -> Unit) {
-    IconButton(onClick) {
+    IconButton(
+        onClick = onClick,
+        modifier =
+        if (isSpaExpressiveEnabled)
+            Modifier
+                .padding(
+                    start = SettingsDimension.paddingLarge,
+                    end = SettingsDimension.paddingSmall,
+                    top = SettingsDimension.paddingExtraSmall,
+                    bottom = SettingsDimension.paddingExtraSmall,
+                )
+                .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
+                .clip(SettingsShape.CornerExtraLarge)
+        else Modifier,
+    ) {
         Icon(
             imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
             contentDescription = contentDescription,
-            modifier = if (isSpaExpressiveEnabled) Modifier
-                .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
-                .clip(SettingsShape.CornerExtraLarge)
-                .background(MaterialTheme.colorScheme.surfaceContainerHigh)
-                .padding(SettingsDimension.actionIconPadding) else Modifier
+            modifier =
+            if (isSpaExpressiveEnabled)
+                Modifier
+                    .size(
+                        SettingsDimension.actionIconWidth,
+                        SettingsDimension.actionIconHeight,
+                    )
+                    .clip(SettingsShape.CornerExtraLarge)
+                    .background(MaterialTheme.colorScheme.surfaceContainerHighest)
+                    .padding(SettingsDimension.actionIconPadding)
+            else Modifier,
         )
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 2ae3b56..2c55779 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -78,7 +78,9 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
 import com.android.settingslib.spa.framework.theme.settingsBackground
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.roundToInt
@@ -116,8 +118,12 @@
 ) {
     TwoRowsTopAppBar(
         title = { Title(title = title, maxLines = 3) },
-        titleTextStyle = MaterialTheme.typography.displaySmall,
-        smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+        titleTextStyle =
+            if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight()
+            else MaterialTheme.typography.displaySmall,
+        smallTitleTextStyle =
+            if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight()
+            else MaterialTheme.typography.titleLarge,
         titleBottomPadding = LargeTitleBottomPadding,
         smallTitle = { Title(title = title, maxLines = 1) },
         modifier = modifier,
@@ -136,7 +142,9 @@
         text = title,
         modifier =
             Modifier.padding(
-                    start = SettingsDimension.itemPaddingAround,
+                    start =
+                        if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall
+                        else SettingsDimension.itemPaddingAround,
                     end = SettingsDimension.itemPaddingEnd,
                 )
                 .semantics { heading() },
@@ -194,7 +202,7 @@
         return lerp(
             containerColor,
             scrolledContainerColor,
-            FastOutLinearInEasing.transform(colorTransitionFraction)
+            FastOutLinearInEasing.transform(colorTransitionFraction),
         )
     }
 
@@ -241,7 +249,7 @@
             Row(
                 horizontalArrangement = Arrangement.End,
                 verticalAlignment = Alignment.CenterVertically,
-                content = actions
+                content = actions,
             )
         }
 
@@ -296,7 +304,7 @@
     windowInsets: WindowInsets,
     colors: TopAppBarColors,
     pinnedHeight: Dp,
-    scrollBehavior: TopAppBarScrollBehavior?
+    scrollBehavior: TopAppBarScrollBehavior?,
 ) {
     if (MaxHeightWithoutTitle <= pinnedHeight) {
         throw IllegalArgumentException(
@@ -333,7 +341,7 @@
             Row(
                 horizontalArrangement = Arrangement.End,
                 verticalAlignment = Alignment.CenterVertically,
-                content = actions
+                content = actions,
             )
         }
     val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
@@ -356,9 +364,9 @@
                         scrollBehavior.state,
                         velocity,
                         scrollBehavior.flingAnimationSpec,
-                        scrollBehavior.snapAnimationSpec
+                        scrollBehavior.snapAnimationSpec,
                     )
-                }
+                },
             )
         } else {
             Modifier
@@ -412,7 +420,8 @@
                                 val measuredMaxHeightPx =
                                     density.run {
                                         MaxHeightWithoutTitle.toPx() +
-                                            coordinates.size.height.toFloat()
+                                            coordinates.size.height.toFloat() +
+                                            titleBaselineHeight.toPx()
                                     }
                                 // Allow larger max height for multi-line title, but do not reduce
                                 // max height to prevent flaky.
@@ -430,7 +439,7 @@
                 titleBottomPadding = titleBottomPaddingPx,
                 hideTitleSemantics = hideBottomRowSemantics,
                 navigationIcon = {},
-                actions = {}
+                actions = {},
             )
         }
     }
@@ -485,7 +494,7 @@
             Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) {
                 CompositionLocalProvider(
                     LocalContentColor provides navigationIconContentColor,
-                    content = navigationIcon
+                    content = navigationIcon,
                 )
             }
             Box(
@@ -504,18 +513,18 @@
                                     fontScale = if (titleScaleDisabled) 1f else fontScale,
                                 )
                             },
-                        content = title
+                        content = title,
                     )
                 }
             }
             Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
                 CompositionLocalProvider(
                     LocalContentColor provides actionIconContentColor,
-                    content = actions
+                    content = actions,
                 )
             }
         },
-        modifier = modifier
+        modifier = modifier,
     ) { measurables, constraints ->
         val navigationIconPlaceable =
             measurables
@@ -552,7 +561,7 @@
             // Navigation icon
             navigationIconPlaceable.placeRelative(
                 x = 0,
-                y = (layoutHeight - navigationIconPlaceable.height) / 2
+                y = (layoutHeight - navigationIconPlaceable.height) / 2,
             )
 
             // Title
@@ -570,17 +579,17 @@
                                     titlePlaceable.height -
                                     max(
                                         0,
-                                        titleBottomPadding - titlePlaceable.height + titleBaseline
+                                        titleBottomPadding - titlePlaceable.height + titleBaseline,
                                     )
                         // Arrangement.Top
                         else -> 0
-                    }
+                    },
             )
 
             // Action icons
             actionIconsPlaceable.placeRelative(
                 x = constraints.maxWidth - actionIconsPlaceable.width,
-                y = (layoutHeight - actionIconsPlaceable.height) / 2
+                y = (layoutHeight - actionIconsPlaceable.height) / 2,
             )
         }
     }
@@ -595,7 +604,7 @@
     state: TopAppBarState,
     velocity: Float,
     flingAnimationSpec: DecayAnimationSpec<Float>?,
-    snapAnimationSpec: AnimationSpec<Float>?
+    snapAnimationSpec: AnimationSpec<Float>?,
 ): Velocity {
     // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
     // and just return Zero Velocity.
@@ -609,20 +618,18 @@
     // continue the motion to expand or collapse the app bar.
     if (flingAnimationSpec != null && abs(velocity) > 1f) {
         var lastValue = 0f
-        AnimationState(
-                initialValue = 0f,
-                initialVelocity = velocity,
-            )
-            .animateDecay(flingAnimationSpec) {
-                val delta = value - lastValue
-                val initialHeightOffset = state.heightOffset
-                state.heightOffset = initialHeightOffset + delta
-                val consumed = abs(initialHeightOffset - state.heightOffset)
-                lastValue = value
-                remainingVelocity = this.velocity
-                // avoid rounding errors and stop if anything is unconsumed
-                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
-            }
+        AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
+            flingAnimationSpec
+        ) {
+            val delta = value - lastValue
+            val initialHeightOffset = state.heightOffset
+            state.heightOffset = initialHeightOffset + delta
+            val consumed = abs(initialHeightOffset - state.heightOffset)
+            lastValue = value
+            remainingVelocity = this.velocity
+            // avoid rounding errors and stop if anything is unconsumed
+            if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+        }
     }
     // Snap if animation specs were provided.
     if (snapAnimationSpec != null) {
@@ -633,7 +640,7 @@
                 } else {
                     state.heightOffsetLimit
                 },
-                animationSpec = snapAnimationSpec
+                animationSpec = snapAnimationSpec,
             ) {
                 state.heightOffset = value
             }
@@ -647,9 +654,10 @@
 // Medium or Large app bar.
 private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
 
-internal val MaxHeightWithoutTitle = 124.dp
+internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp
 internal val DefaultTitleHeight = 52.dp
 internal val ContainerHeight = 56.dp
+private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp
 private val LargeTitleBottomPadding = 28.dp
 private val TopAppBarHorizontalPadding = 4.dp
 
diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp
index e70201b..76e36dc 100644
--- a/packages/SettingsLib/TopIntroPreference/Android.bp
+++ b/packages/SettingsLib/TopIntroPreference/Android.bp
@@ -14,7 +14,10 @@
         "SettingsLintDefaults",
     ],
 
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     resource_dirs: ["res"],
 
     static_libs: [
diff --git a/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml
new file mode 100644
index 0000000..fb13ef7
--- /dev/null
+++ b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <com.android.settingslib.widget.CollapsableTextView
+        android:id="@+id/collapsable_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java
deleted file mode 100644
index 1bbd76d..0000000
--- a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settingslib.widget.preference.topintro.R;
-
-/**
- * The TopIntroPreference shows a text which describe a feature. Gernerally, we expect this
- * preference always shows on the top of screen.
- */
-public class TopIntroPreference extends Preference {
-
-    public TopIntroPreference(Context context) {
-        super(context);
-        setLayoutResource(R.layout.top_intro_preference);
-        setSelectable(false);
-    }
-
-    public TopIntroPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setLayoutResource(R.layout.top_intro_preference);
-        setSelectable(false);
-    }
-
-    @Override
-    public void onBindViewHolder(PreferenceViewHolder holder) {
-        super.onBindViewHolder(holder);
-        holder.setDividerAllowedAbove(false);
-        holder.setDividerAllowedBelow(false);
-    }
-}
diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
new file mode 100644
index 0000000..afced0c
--- /dev/null
+++ b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import androidx.annotation.RequiresApi
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.topintro.R
+
+open class TopIntroPreference @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+    private var isCollapsable: Boolean = false
+    private var minLines: Int = 2
+
+    init {
+        if (SettingsThemeHelper.isExpressiveTheme(context)) {
+            layoutResource = R.layout.settingslib_expressive_top_intro
+            initAttributes(context, attrs, defStyleAttr)
+        } else {
+            layoutResource = R.layout.top_intro_preference
+        }
+        isSelectable = false
+    }
+
+    private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
+        context.obtainStyledAttributes(
+            attrs,
+            COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0
+        ).apply {
+            isCollapsable = getBoolean(IS_COLLAPSABLE, false)
+            minLines = getInt(
+                MIN_LINES,
+                if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
+            ).coerceIn(1, DEFAULT_MAX_LINES)
+            recycle()
+        }
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        holder.isDividerAllowedAbove = false
+        holder.isDividerAllowedBelow = false
+
+        if (!SettingsThemeHelper.isExpressiveTheme(context)) {
+            return
+        }
+
+        (holder.findViewById(R.id.collapsable_text_view) as? CollapsableTextView)?.apply {
+            setCollapsable(isCollapsable)
+            setMinLines(minLines)
+            setText(title.toString())
+        }
+    }
+
+    /**
+     * Sets whether the text view is collapsable.
+     * @param collapsable True if the text view should be collapsable, false otherwise.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    fun setCollapsable(collapsable: Boolean) {
+        isCollapsable = collapsable
+        notifyChanged()
+    }
+
+    /**
+     * Sets the minimum number of lines to display when collapsed.
+     * @param lines The minimum number of lines.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    fun setMinLines(lines: Int) {
+        minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
+        notifyChanged()
+    }
+
+    companion object {
+        private const val DEFAULT_MAX_LINES = 10
+        private const val DEFAULT_MIN_LINES = 2
+
+        private val COLLAPSABLE_TEXT_VIEW_ATTRS =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView
+        private val MIN_LINES =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines
+        private val IS_COLLAPSABLE =
+            com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable
+    }
+}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 34e33c0..efc98db 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1645,13 +1645,13 @@
     <string name="media_transfer_headphone_name">Headphone</string>
 
     <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] -->
-    <string name="media_transfer_usb_speaker_name">USB speaker</string>
+    <string name="media_transfer_usb_audio_name">USB audio</string>
 
     <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] -->
     <string name="media_transfer_wired_device_mic_name">Mic jack</string>
 
     <!-- Name of the usb audio device mic. [CHAR LIMIT=50] -->
-    <string name="media_transfer_usb_device_mic_name">USB mic</string>
+    <string name="media_transfer_usb_device_mic_name">USB microphone</string>
 
     <!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] -->
     <string name="wifi_hotspot_switch_on_text">On</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
index 58dc8c7..e7c7476 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
@@ -45,6 +45,7 @@
             DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
             DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER,
             DeviceSettingId.DEVICE_SETTING_ID_ANC,
+            DeviceSettingId.DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER,
         },
         open = true)
 public @interface DeviceSettingId {
@@ -114,6 +115,9 @@
     /** Device setting ID for "More Settings" page. */
     int DEVICE_SETTING_ID_MORE_SETTINGS = 21;
 
+    /** Device setting ID for general bluetooth device header. */
+    int DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER = 22;
+
     /** Device setting ID for ANC. */
     int DEVICE_SETTING_ID_ANC = 1001;
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
index 38183d5..da01b3b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
@@ -32,9 +32,9 @@
  */
 data class DeviceSettingItem(
     @DeviceSettingId val settingId: Int,
-    val packageName: String,
-    val className: String,
-    val intentAction: String,
+    val packageName: String? = null,
+    val className: String? = null,
+    val intentAction: String? = null,
     val preferenceKey: String? = null,
     val highlighted: Boolean = false,
     val extras: Bundle = Bundle.EMPTY,
@@ -62,11 +62,11 @@
                     parcel.run {
                         DeviceSettingItem(
                             settingId = readInt(),
-                            packageName = readString() ?: "",
-                            className = readString() ?: "",
-                            intentAction = readString() ?: "",
+                            packageName = readString(),
+                            className = readString(),
+                            intentAction = readString(),
                             highlighted = readBoolean(),
-                            preferenceKey = readString() ?: "",
+                            preferenceKey = readString(),
                             extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
                         )
                     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
new file mode 100644
index 0000000..d837806
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSettingsConfigServiceStatus;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
new file mode 100644
index 0000000..ae86771
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * A data class representing a device settings config service status.
+ *
+ * @property success Whether the status is succeed.
+ * @property extras Extra bundle
+ */
+data class DeviceSettingsConfigServiceStatus(
+    val success: Boolean,
+    val extras: Bundle = Bundle.EMPTY,
+) : Parcelable {
+
+    override fun describeContents(): Int = 0
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.run {
+            writeBoolean(success)
+            writeBundle(extras)
+        }
+    }
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<DeviceSettingsConfigServiceStatus> =
+            object : Parcelable.Creator<DeviceSettingsConfigServiceStatus> {
+                override fun createFromParcel(parcel: Parcel) =
+                    parcel.run {
+                        DeviceSettingsConfigServiceStatus(
+                            success = readBoolean(),
+                            extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
+                        )
+                    }
+
+                override fun newArray(size: Int): Array<DeviceSettingsConfigServiceStatus?> {
+                    return arrayOfNulls(size)
+                }
+            }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
index 977849e..77d790e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
@@ -21,7 +21,7 @@
 import android.os.Parcelable
 
 /**
- * A data class representing a device settings item in bluetooth device details config.
+ * A data class representing a device settings provider service status.
  *
  * @property enabled Whether the service is enabled.
  * @property extras Extra bundle
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
index 647611e..9cf4907 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
@@ -17,8 +17,8 @@
 package com.android.settingslib.bluetooth.devicesettings;
 
 import com.android.settingslib.bluetooth.devicesettings.DeviceInfo;
-import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback;
 
 interface IDeviceSettingsConfigProviderService {
-   DeviceSettingsConfig getDeviceSettingsConfig(in DeviceInfo device);
+   oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback);
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
new file mode 100644
index 0000000..403cdd9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus;
+
+interface IGetDeviceSettingsConfigCallback {
+   oneway void onResult(in DeviceSettingsConfigServiceStatus status, in DeviceSettingsConfig config) = 0;
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 3d8ff86..4af0504 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -33,12 +33,15 @@
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
 import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +66,7 @@
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
+import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -74,22 +78,22 @@
     private val backgroundCoroutineContext: CoroutineContext,
 ) {
     data class EndPoint(
-        private val packageName: String,
+        private val packageName: String?,
         private val className: String?,
-        private val intentAction: String,
+        private val intentAction: String?,
     ) {
-        fun toIntent(): Intent =
-            Intent().apply {
+        fun toIntent(): Intent? {
+            if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(intentAction)) {
+                return null
+            }
+            return Intent().apply {
                 if (className.isNullOrBlank()) {
                     setPackage(packageName)
                 } else {
-                    setClassName(packageName, className)
+                    setClassName(packageName!!, className)
                 }
                 setAction(intentAction)
             }
-
-        fun isValid(): Boolean {
-            return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
         }
     }
 
@@ -126,8 +130,9 @@
                     when (it) {
                         is ServiceConnectionStatus.Connected ->
                             flowOf(
-                                it.service.getDeviceSettingsConfig(
-                                    deviceInfo { setBluetoothAddress(cachedDevice.address) }
+                                getDeviceSettingsConfigFromService(
+                                    deviceInfo { setBluetoothAddress(cachedDevice.address) },
+                                    it.service,
                                 )
                             )
                         ServiceConnectionStatus.Connecting -> flowOf()
@@ -137,6 +142,27 @@
                 .first()
         }
 
+    private suspend fun getDeviceSettingsConfigFromService(
+        deviceInfo: DeviceInfo,
+        service: IDeviceSettingsConfigProviderService,
+    ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation ->
+        service.getDeviceSettingsConfig(
+            deviceInfo,
+            object : IGetDeviceSettingsConfigCallback.Stub() {
+                override fun onResult(
+                    status: DeviceSettingsConfigServiceStatus,
+                    config: DeviceSettingsConfig?,
+                ) {
+                    if (!status.success) {
+                        continuation.resume(null)
+                    } else {
+                        continuation.resume(config)
+                    }
+                }
+            },
+        )
+    }
+
     private val settingIdToItemMapping =
         flow {
                 if (!isServiceEnabled.await()) {
@@ -160,6 +186,12 @@
             }
             .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1)
 
+    private val services =
+        ConcurrentHashMap<
+            EndPoint,
+            StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
+        >()
+
     /** Gets [DeviceSettingsConfig] for the device, return null when failed. */
     suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? {
         if (!isServiceEnabled.await()) {
@@ -222,24 +254,23 @@
                     )
                 }
             }
-            ?.filter { it.isValid() }
             ?.distinct()
-            ?.associateBy(
-                { it },
-                { endpoint ->
-                    services.computeIfAbsent(endpoint) {
-                        getService(
-                                endpoint.toIntent(),
-                                IDeviceSettingsProviderService.Stub::asInterface,
-                            )
-                            .stateIn(
-                                coroutineScope.plus(backgroundCoroutineContext),
-                                SharingStarted.WhileSubscribed(),
-                                ServiceConnectionStatus.Connecting,
-                            )
-                    }
-                },
-            )
+            ?.mapNotNull { endpoint ->
+                endpoint.toIntent()?.let { intent ->
+                    Pair(
+                        endpoint,
+                        services.computeIfAbsent(endpoint) {
+                            getService(intent, IDeviceSettingsProviderService.Stub::asInterface)
+                                .stateIn(
+                                    coroutineScope.plus(backgroundCoroutineContext),
+                                    SharingStarted.WhileSubscribed(),
+                                    ServiceConnectionStatus.Connecting,
+                                )
+                        },
+                    )
+                }
+            }
+            ?.toMap()
 
     private fun getDeviceSettingsFromService(
         cachedDevice: CachedBluetoothDevice,
@@ -320,11 +351,5 @@
         const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME"
         const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS"
         const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION"
-
-        val services =
-            ConcurrentHashMap<
-                EndPoint,
-                StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
-            >()
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index ef0f6cb..13a0601 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -42,6 +42,8 @@
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
+import java.util.Objects;
+
 /**
  * Drawable displaying a mobile cell signal indicator.
  */
@@ -90,6 +92,10 @@
     private int mCurrentDot;
 
     public SignalDrawable(Context context) {
+        this(context, new Handler());
+    }
+
+    public SignalDrawable(@NonNull Context context, @NonNull Handler handler) {
         super(context.getDrawable(ICON_RES));
         final String attributionPathString = context.getString(
                 com.android.internal.R.string.config_signalAttributionPath);
@@ -106,7 +112,7 @@
         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
-        mHandler = new Handler();
+        mHandler = handler;
         setDarkIntensity(0);
     }
 
@@ -304,6 +310,17 @@
                 | level;
     }
 
+    @Override
+    public boolean equals(@Nullable Object other) {
+        return other instanceof SignalDrawable
+                && ((SignalDrawable) other).getLevel() == this.getLevel();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getLevel());
+    }
+
     /** Returns the state representing empty mobile signal with the given number of levels. */
     public static int getEmptyState(int numLevels) {
         return getState(0, numLevels, true);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 0b8fb22ce..feaf7fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -97,7 +97,7 @@
             case TYPE_USB_ACCESSORY:
                 name =
                         inputRoutingEnabledAndIsDesktop()
-                                ? context.getString(R.string.media_transfer_usb_speaker_name)
+                                ? context.getString(R.string.media_transfer_usb_audio_name)
                                 : context.getString(R.string.media_transfer_wired_headphone_name);
                 break;
             case TYPE_DOCK:
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
new file mode 100644
index 0000000..3149acf
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceSettingsConfigServiceStatusTest {
+
+    @Test
+    fun parcelOperation() {
+        val item =
+            DeviceSettingsConfigServiceStatus(
+                success = true,
+                extras = Bundle().apply { putString("key1", "value1") },
+            )
+
+        val fromParcel = writeAndRead(item)
+
+        assertThat(fromParcel.success).isEqualTo(item.success)
+        assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1"))
+    }
+
+    private fun writeAndRead(
+        item: DeviceSettingsConfigServiceStatus
+    ): DeviceSettingsConfigServiceStatus {
+        val parcel = Parcel.obtain()
+        item.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+        return DeviceSettingsConfigServiceStatus.CREATOR.createFromParcel(parcel)
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 0cb6bc1..4e62fd3b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -33,10 +33,12 @@
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
 import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState
 import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
@@ -53,7 +55,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -140,15 +141,10 @@
             )
     }
 
-    @After
-    fun clean() {
-        DeviceSettingServiceConnection.services.clear()
-    }
-
     @Test
     fun getDeviceSettingsConfig_withMetadata_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(true))
             `when`(settingProviderService2.serviceStatus)
@@ -179,7 +175,7 @@
                     )
                 )
                 .thenReturn("".toByteArray())
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(true))
             `when`(settingProviderService2.serviceStatus)
@@ -194,7 +190,7 @@
     @Test
     fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.serviceStatus)
                 .thenReturn(DeviceSettingsProviderServiceStatus(false))
             `when`(settingProviderService2.serviceStatus)
@@ -209,7 +205,7 @@
     @Test
     fun getDeviceSettingsConfig_bindingServiceFail_returnNull() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any())
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -221,7 +217,7 @@
     @Test
     fun getDeviceSetting_actionSwitchPreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -247,7 +243,7 @@
     @Test
     fun getDeviceSetting_multiTogglePreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -273,7 +269,7 @@
     @Test
     fun getDeviceSetting_helpPreference_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -299,6 +295,7 @@
     @Test
     fun getDeviceSetting_noConfig_returnNull() {
         testScope.runTest {
+            setUpConfigService(false, null)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -320,7 +317,7 @@
     @Test
     fun updateDeviceSettingState_switchState_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -358,7 +355,7 @@
     @Test
     fun updateDeviceSettingState_multiToggleState_success() {
         testScope.runTest {
-            `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+            setUpConfigService(true, DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
                 input ->
                 input
@@ -459,6 +456,17 @@
         assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
     }
 
+    private fun setUpConfigService(success: Boolean, config: DeviceSettingsConfig?) {
+        `when`(configService.getDeviceSettingsConfig(any(), any())).then { input ->
+            input
+                .getArgument<IGetDeviceSettingsConfigCallback>(1)
+                .onResult(
+                    DeviceSettingsConfigServiceStatus(success = success),
+                    config
+                )
+        }
+    }
+
     private companion object {
         const val BLUETOOTH_ADDRESS = "12:34:56:78"
         const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice"
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index da5f428..1739c0e5 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -136,7 +136,7 @@
         when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE);
 
         assertThat(mPhoneMediaDevice.getName())
-                .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name));
+                .isEqualTo(mContext.getString(R.string.media_transfer_usb_audio_name));
 
         when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d710939..5e31da4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -164,6 +164,7 @@
         Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
         Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
         Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+        Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
         Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
         Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
         Settings.Secure.ZEN_DURATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index fa16a44..b3f7374 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -243,6 +243,7 @@
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index f8383d9..88cc152 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -40,16 +40,6 @@
 }
 
 flag {
-   name: "notification_minimalism_prototype"
-   namespace: "systemui"
-   description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
-   bug: "330387368"
-   metadata {
-        purpose: PURPOSE_BUGFIX
-   }
-}
-
-flag {
    name: "notification_view_flipper_pausing_v2"
    namespace: "systemui"
    description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
new file mode 100644
index 0000000..08db95e
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
+import android.window.WindowAnimationState;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.shared.TransitionUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
+ * and automatically attaches it to the transition leash before the transition starts.
+ */
+public class OriginRemoteTransition extends IRemoteTransition.Stub {
+    private static final String TAG = "OriginRemoteTransition";
+
+    private final Context mContext;
+    private final boolean mIsEntry;
+    private final UIComponent mOrigin;
+    private final TransitionPlayer mPlayer;
+    private final long mDuration;
+    private final Handler mHandler;
+
+    @Nullable private SurfaceControl.Transaction mStartTransaction;
+    @Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
+    @Nullable private UIComponent.Transaction mOriginTransaction;
+    @Nullable private ValueAnimator mAnimator;
+    @Nullable private SurfaceControl mOriginLeash;
+    private boolean mCancelled;
+
+    OriginRemoteTransition(
+            Context context,
+            boolean isEntry,
+            UIComponent origin,
+            TransitionPlayer player,
+            long duration,
+            Handler handler) {
+        mContext = context;
+        mIsEntry = isEntry;
+        mOrigin = origin;
+        mPlayer = player;
+        mDuration = duration;
+        mHandler = handler;
+    }
+
+    @Override
+    public void startAnimation(
+            IBinder token,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("startAnimation - " + info);
+        mHandler.post(
+                () -> {
+                    mStartTransaction = t;
+                    mFinishCallback = finishCallback;
+                    startAnimationInternal(info);
+                });
+    }
+
+    @Override
+    public void mergeAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IBinder mergeTarget,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("mergeAnimation - " + info);
+        mHandler.post(this::cancel);
+    }
+
+    @Override
+    public void takeOverAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback,
+            WindowAnimationState[] states) {
+        logD("takeOverAnimation - " + info);
+    }
+
+    @Override
+    public void onTransitionConsumed(IBinder transition, boolean aborted) {
+        logD("onTransitionConsumed - aborted: " + aborted);
+        mHandler.post(this::cancel);
+    }
+
+    private void startAnimationInternal(TransitionInfo info) {
+        if (!prepareUIs(info)) {
+            logE("Unable to prepare UI!");
+            finishAnimation(/* finished= */ false);
+            return;
+        }
+        // Notify player that we are starting.
+        mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction);
+
+        // Start the animator.
+        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mAnimator.setDuration(mDuration);
+        mAnimator.addListener(
+                new AnimatorListener() {
+                    @Override
+                    public void onAnimationStart(Animator a) {}
+
+                    @Override
+                    public void onAnimationEnd(Animator a) {
+                        finishAnimation(/* finished= */ !mCancelled);
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator a) {
+                        mCancelled = true;
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animator a) {}
+                });
+        mAnimator.addUpdateListener(
+                a -> {
+                    mPlayer.onProgress((float) a.getAnimatedValue());
+                });
+        mAnimator.start();
+    }
+
+    private boolean prepareUIs(TransitionInfo info) {
+        if (info.getRootCount() == 0) {
+            logE("prepareUIs: no root leash!");
+            return false;
+        }
+        if (info.getRootCount() > 1) {
+            logE("prepareUIs: multi-display transition is not supported yet!");
+            return false;
+        }
+        if (info.getChanges().isEmpty()) {
+            logE("prepareUIs: no changes!");
+            return false;
+        }
+
+        SurfaceControl rootLeash = info.getRoot(0).getLeash();
+        int displayId = info.getChanges().get(0).getEndDisplayId();
+        Rect displayBounds = getDisplayBounds(displayId);
+        float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);
+
+        // Create the origin leash and add to the transition root leash.
+        mOriginLeash =
+                new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
+        mStartTransaction
+                .reparent(mOriginLeash, rootLeash)
+                .show(mOriginLeash)
+                .setCornerRadius(mOriginLeash, windowRadius)
+                .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());
+
+        // Process surfaces
+        List<SurfaceControl> openingSurfaces = new ArrayList<>();
+        List<SurfaceControl> closingSurfaces = new ArrayList<>();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            SurfaceControl leash = change.getLeash();
+            // Reparent leash to the transition root.
+            mStartTransaction.reparent(leash, rootLeash);
+            if (TransitionUtil.isOpeningMode(mode)) {
+                openingSurfaces.add(change.getLeash());
+                // For opening surfaces, ending bounds are base bound. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getEndAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            } else if (TransitionUtil.isClosingMode(mode)) {
+                closingSurfaces.add(change.getLeash());
+                // For closing surfaces, starting bounds are base bounds. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getStartAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            }
+        }
+
+        // Set relative order:
+        // ----  App1  ----
+        // ---- origin ----
+        // ----  App2  ----
+        if (mIsEntry) {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
+        } else {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
+        }
+
+        // Attach origin UIComponent to origin leash.
+        mOriginTransaction = mOrigin.newTransaction();
+        mOriginTransaction
+                .attachToTransitionLeash(
+                        mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height())
+                .commit();
+
+        // Apply all surface changes.
+        mStartTransaction.apply();
+        return true;
+    }
+
+    private Rect getDisplayBounds(int displayId) {
+        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+        DisplayMetrics metrics = new DisplayMetrics();
+        dm.getDisplay(displayId).getMetrics(metrics);
+        return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
+    }
+
+    private void finishAnimation(boolean finished) {
+        logD("finishAnimation: finished=" + finished);
+        if (mAnimator == null) {
+            // The transition didn't start. Ensure we apply the start transaction and report
+            // finish afterwards.
+            mStartTransaction
+                    .addTransactionCommittedListener(
+                            mContext.getMainExecutor(), this::finishInternal)
+                    .apply();
+            return;
+        }
+        mAnimator = null;
+        // Notify client that we have ended.
+        mPlayer.onEnd(finished);
+        // Detach the origin from the transition leash and report finish after it's done.
+        mOriginTransaction
+                .detachFromTransitionLeash(
+                        mOrigin, mContext.getMainExecutor(), this::finishInternal)
+                .commit();
+    }
+
+    private void finishInternal() {
+        logD("finishInternal");
+        if (mOriginLeash != null) {
+            // Release origin leash.
+            mOriginLeash.release();
+            mOriginLeash = null;
+        }
+        try {
+            mFinishCallback.onTransitionFinished(null, null);
+        } catch (RemoteException e) {
+            logE("Unable to report transition finish!", e);
+        }
+        mStartTransaction = null;
+        mOriginTransaction = null;
+        mFinishCallback = null;
+    }
+
+    private void cancel() {
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+    }
+
+    private static void logD(String msg) {
+        if (OriginTransitionSession.DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private static void logE(String msg) {
+        Log.e(TAG, msg);
+    }
+
+    private static void logE(String msg, Throwable e) {
+        Log.e(TAG, msg, e);
+    }
+
+    private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
+        List<SurfaceControl> surfaces = new ArrayList<>();
+        Rect maxBounds = new Rect();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            if (TransitionUtil.isOpeningMode(mode) == isOpening) {
+                surfaces.add(change.getLeash());
+                Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
+                maxBounds.union(bounds);
+            }
+        }
+        return new SurfaceUIComponent(
+                surfaces,
+                /* alpha= */ 1.0f,
+                /* visible= */ true,
+                /* bounds= */ maxBounds,
+                /* baseBounds= */ maxBounds);
+    }
+
+    /** An interface that represents an origin transitions. */
+    public interface TransitionPlayer {
+
+        /**
+         * Called when an origin transition starts. This method exposes the raw {@link
+         * TransitionInfo} so that clients can extract more information from it.
+         */
+        default void onStart(
+                TransitionInfo transitionInfo,
+                SurfaceControl.Transaction sfTransaction,
+                UIComponent origin,
+                UIComponent.Transaction uiTransaction) {
+            // Wrap transactions.
+            Transactions transactions =
+                    new Transactions()
+                            .registerTransactionForClass(origin.getClass(), uiTransaction)
+                            .registerTransactionForClass(
+                                    SurfaceUIComponent.class,
+                                    new SurfaceUIComponent.Transaction(sfTransaction));
+            // Wrap surfaces and start.
+            onStart(
+                    transactions,
+                    origin,
+                    wrapSurfaces(transitionInfo, /* isOpening= */ false),
+                    wrapSurfaces(transitionInfo, /* isOpening= */ true));
+        }
+
+        /**
+         * Called when an origin transition starts. This method exposes the opening and closing
+         * windows as wrapped {@link UIComponent} to provide simplified interface to clients.
+         */
+        void onStart(
+                UIComponent.Transaction transaction,
+                UIComponent origin,
+                UIComponent closingApp,
+                UIComponent openingApp);
+
+        /** Called to update the transition frame. */
+        void onProgress(float progress);
+
+        /** Called when the transition ended. */
+        void onEnd(boolean finished);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
index 64bedd3..23693b6 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -24,11 +24,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 import android.window.IRemoteTransition;
 import android.window.RemoteTransition;
 
+import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer;
 import com.android.systemui.animation.shared.IOriginTransitions;
 
 import java.lang.annotation.Retention;
@@ -182,6 +185,7 @@
         @Nullable private final IOriginTransitions mOriginTransitions;
         @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
         @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+        private Handler mHandler = new Handler(Looper.getMainLooper());
         private String mName;
         @Nullable private Predicate<RemoteTransition> mIntentStarter;
 
@@ -259,12 +263,48 @@
             return this;
         }
 
+        /** Add an origin entry transition to the builder. */
+        public Builder withEntryTransition(
+                UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) {
+            mEntryTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ true,
+                                    entryOrigin,
+                                    entryPlayer,
+                                    entryDuration,
+                                    mHandler);
+            return this;
+        }
+
         /** Add an exit transition to the builder. */
         public Builder withExitTransition(IRemoteTransition transition) {
             mExitTransitionSupplier = () -> transition;
             return this;
         }
 
+        /** Add an origin exit transition to the builder. */
+        public Builder withExitTransition(
+                UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) {
+            mExitTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ false,
+                                    exitTarget,
+                                    exitPlayer,
+                                    exitDuration,
+                                    mHandler);
+            return this;
+        }
+
+        /** Supply a handler where transition callbacks will run. */
+        public Builder withHandler(Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
         /** Build an {@link OriginTransitionSession}. */
         public OriginTransitionSession build() {
             if (mIntentStarter == null) {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
new file mode 100644
index 0000000..2438736
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+/** A {@link UIComponent} representing a {@link SurfaceControl}. */
+public class SurfaceUIComponent implements UIComponent {
+    private final Collection<SurfaceControl> mSurfaces;
+    private final Rect mBaseBounds;
+    private final float[] mFloat9 = new float[9];
+
+    private float mAlpha;
+    private boolean mVisible;
+    private Rect mBounds;
+
+    public SurfaceUIComponent(
+            SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) {
+        this(Arrays.asList(sc), alpha, visible, bounds, baseBounds);
+    }
+
+    public SurfaceUIComponent(
+            Collection<SurfaceControl> surfaces,
+            float alpha,
+            boolean visible,
+            Rect bounds,
+            Rect baseBounds) {
+        mSurfaces = surfaces;
+        mAlpha = alpha;
+        mVisible = visible;
+        mBounds = bounds;
+        mBaseBounds = baseBounds;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mVisible;
+    }
+
+    @Override
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction(new SurfaceControl.Transaction());
+    }
+
+    @Override
+    public String toString() {
+        return "SurfaceUIComponent{mSurfaces="
+                + mSurfaces
+                + ", mAlpha="
+                + mAlpha
+                + ", mVisible="
+                + mVisible
+                + ", mBounds="
+                + mBounds
+                + ", mBaseBounds="
+                + mBaseBounds
+                + "}";
+    }
+
+    /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */
+    public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> {
+        private final SurfaceControl.Transaction mTransaction;
+        private final ArrayList<Runnable> mChanges = new ArrayList<>();
+
+        public Transaction(SurfaceControl.Transaction transaction) {
+            mTransaction = transaction;
+        }
+
+        @Override
+        public Transaction setAlpha(SurfaceUIComponent ui, float alpha) {
+            mChanges.add(
+                    () -> {
+                        ui.mAlpha = alpha;
+                        ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(SurfaceUIComponent ui, boolean visible) {
+            mChanges.add(
+                    () -> {
+                        ui.mVisible = visible;
+                        if (visible) {
+                            ui.mSurfaces.forEach(s -> mTransaction.show(s));
+                        } else {
+                            ui.mSurfaces.forEach(s -> mTransaction.hide(s));
+                        }
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) {
+            mChanges.add(
+                    () -> {
+                        if (ui.mBounds.equals(bounds)) {
+                            return;
+                        }
+                        ui.mBounds = bounds;
+                        Matrix matrix = new Matrix();
+                        matrix.setRectToRect(
+                                new RectF(ui.mBaseBounds),
+                                new RectF(ui.mBounds),
+                                Matrix.ScaleToFit.FILL);
+                        ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(
+                    () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash)));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                SurfaceUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(
+                    () -> {
+                        ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null));
+                        mTransaction.addTransactionCommittedListener(executor, onDone::run);
+                    });
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+            mTransaction.apply();
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
new file mode 100644
index 0000000..5240d99
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * A composite {@link UIComponent.Transaction} that combines multiple other transactions for each ui
+ * type.
+ */
+public class Transactions implements UIComponent.Transaction<UIComponent> {
+    private final Map<Class, UIComponent.Transaction> mTransactions = new ArrayMap<>();
+
+    /** Register a transaction object for updating a certain {@link UIComponent} type. */
+    public <T extends UIComponent> Transactions registerTransactionForClass(
+            Class<T> clazz, UIComponent.Transaction transaction) {
+        mTransactions.put(clazz, transaction);
+        return this;
+    }
+
+    private UIComponent.Transaction getTransactionFor(UIComponent ui) {
+        UIComponent.Transaction transaction = mTransactions.get(ui.getClass());
+        if (transaction == null) {
+            transaction = ui.newTransaction();
+            mTransactions.put(ui.getClass(), transaction);
+        }
+        return transaction;
+    }
+
+    @Override
+    public Transactions setAlpha(UIComponent ui, @FloatRange(from = 0.0, to = 1.0) float alpha) {
+        getTransactionFor(ui).setAlpha(ui, alpha);
+        return this;
+    }
+
+    @Override
+    public Transactions setVisible(UIComponent ui, boolean visible) {
+        getTransactionFor(ui).setVisible(ui, visible);
+        return this;
+    }
+
+    @Override
+    public Transactions setBounds(UIComponent ui, Rect bounds) {
+        getTransactionFor(ui).setBounds(ui, bounds);
+        return this;
+    }
+
+    @Override
+    public Transactions attachToTransitionLeash(
+            UIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+        getTransactionFor(ui).attachToTransitionLeash(ui, transitionLeash, w, h);
+        return this;
+    }
+
+    @Override
+    public Transactions detachFromTransitionLeash(
+            UIComponent ui, Executor executor, Runnable onDone) {
+        getTransactionFor(ui).detachFromTransitionLeash(ui, executor, onDone);
+        return this;
+    }
+
+    @Override
+    public void commit() {
+        mTransactions.values().forEach(UIComponent.Transaction::commit);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
new file mode 100644
index 0000000..747e4d1
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import java.util.concurrent.Executor;
+
+/** An interface representing an UI component on the display. */
+public interface UIComponent {
+
+    /** Get the current alpha of this UI. */
+    float getAlpha();
+
+    /** Check if this UI is visible. */
+    boolean isVisible();
+
+    /** Get the bounds of this UI in its display. */
+    Rect getBounds();
+
+    /** Create a new {@link Transaction} that can update this UI. */
+    Transaction newTransaction();
+
+    /**
+     * A transaction class for updating {@link UIComponent}.
+     *
+     * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle.
+     */
+    interface Transaction<T extends UIComponent> {
+        /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha);
+
+        /**
+         * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called.
+         */
+        Transaction setVisible(T ui, boolean visible);
+
+        /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setBounds(T ui, Rect bounds);
+
+        /**
+         * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is
+         * called.
+         */
+        Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h);
+
+        /**
+         * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is
+         * called.
+         */
+        Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone);
+
+        /** Commit any pending changes added to this transaction. */
+        void commit();
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
new file mode 100644
index 0000000..313789c
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
+ * class will draw the content of the {@link View} directly into the leash, and the actual View will
+ * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
+ * full-screen size leash without being constrained by the view tree's boundary or inheriting its
+ * parent's alpha and transformation.
+ */
+public class ViewUIComponent implements UIComponent {
+    private static final String TAG = "ViewUIComponent";
+    private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+    private final OnDrawListener mOnDrawListener = this::postDraw;
+    private final View mView;
+
+    @Nullable private SurfaceControl mSurfaceControl;
+    @Nullable private Surface mSurface;
+    @Nullable private Rect mViewBoundsOverride;
+    private boolean mVisibleOverride;
+    private boolean mDirty;
+
+    public ViewUIComponent(View view) {
+        mView = view;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mView.getAlpha();
+    }
+
+    @Override
+    public boolean isVisible() {
+        return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
+    }
+
+    @Override
+    public Rect getBounds() {
+        if (isAttachedToLeash() && mViewBoundsOverride != null) {
+            return mViewBoundsOverride;
+        }
+        return getRealBounds();
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction();
+    }
+
+    private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
+        logD("attachToTransitionLeash");
+        // Remember current visibility.
+        mVisibleOverride = mView.getVisibility() == View.VISIBLE;
+
+        // Create the surface
+        mSurfaceControl =
+                new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
+        mSurface = new Surface(mSurfaceControl);
+        forceDraw();
+
+        // Attach surface to transition leash
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
+
+        // Make sure view draw triggers surface draw.
+        mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+
+        // Make the view invisible AFTER the surface is shown.
+        t.addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> mView.setVisibility(View.INVISIBLE))
+                .apply();
+    }
+
+    private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
+        logD("detachFromTransitionLeash");
+        Surface s = mSurface;
+        SurfaceControl sc = mSurfaceControl;
+        mSurface = null;
+        mSurfaceControl = null;
+        mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+        // Restore view visibility
+        mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
+        mView.invalidate();
+        // Clean up surfaces.
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(sc, null)
+                .addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> {
+                            s.release();
+                            sc.release();
+                            executor.execute(onDone);
+                        });
+        // Apply transaction AFTER the view is drawn.
+        mView.getRootSurfaceControl().applyTransactionOnDraw(t);
+    }
+
+    @Override
+    public String toString() {
+        return "ViewUIComponent{"
+                + "alpha="
+                + getAlpha()
+                + ", visible="
+                + isVisible()
+                + ", bounds="
+                + getBounds()
+                + ", attached="
+                + isAttachedToLeash()
+                + "}";
+    }
+
+    private void draw() {
+        if (!mDirty) {
+            // No need to draw. This is probably a duplicate call.
+            logD("draw: skipped - clean");
+            return;
+        }
+        mDirty = false;
+        if (!isAttachedToLeash()) {
+            // Not attached.
+            logD("draw: skipped - not attached");
+            return;
+        }
+        ViewGroup.LayoutParams params = mView.getLayoutParams();
+        if (params == null || params.width == 0 || params.height == 0) {
+            // layout pass didn't happen.
+            logD("draw: skipped - no layout");
+            return;
+        }
+        Canvas canvas = mSurface.lockHardwareCanvas();
+        // Clear the canvas first.
+        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        if (mVisibleOverride) {
+            Rect realBounds = getRealBounds();
+            Rect renderBounds = getBounds();
+            canvas.translate(renderBounds.left, renderBounds.top);
+            canvas.scale(
+                    (float) renderBounds.width() / realBounds.width(),
+                    (float) renderBounds.height() / realBounds.height());
+            canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
+            mView.draw(canvas);
+            canvas.restore();
+        }
+        mSurface.unlockCanvasAndPost(canvas);
+        logD("draw: done");
+    }
+
+    private void forceDraw() {
+        mDirty = true;
+        draw();
+    }
+
+    private Rect getRealBounds() {
+        Rect output = new Rect();
+        mView.getBoundsOnScreen(output);
+        return output;
+    }
+
+    private boolean isAttachedToLeash() {
+        return mSurfaceControl != null && mSurface != null;
+    }
+
+    private void logD(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private void setVisible(boolean visible) {
+        logD("setVisibility: " + visible);
+        if (isAttachedToLeash()) {
+            mVisibleOverride = visible;
+            postDraw();
+        } else {
+            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    private void setBounds(Rect bounds) {
+        logD("setBounds: " + bounds);
+        mViewBoundsOverride = bounds;
+        if (isAttachedToLeash()) {
+            postDraw();
+        } else {
+            Log.w(TAG, "setBounds: not attached to leash!");
+        }
+    }
+
+    private void setAlpha(float alpha) {
+        logD("setAlpha: " + alpha);
+        mView.setAlpha(alpha);
+        if (isAttachedToLeash()) {
+            postDraw();
+        }
+    }
+
+    private void postDraw() {
+        if (mDirty) {
+            return;
+        }
+        mDirty = true;
+        mView.post(this::draw);
+    }
+
+    public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
+        private final List<Runnable> mChanges = new ArrayList<>();
+
+        @Override
+        public Transaction setAlpha(ViewUIComponent ui, float alpha) {
+            mChanges.add(() -> ui.setAlpha(alpha));
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(ViewUIComponent ui, boolean visible) {
+            mChanges.add(() -> ui.setVisible(visible));
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
+            mChanges.add(() -> ui.setBounds(bounds));
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(() -> ui.attachToTransitionLeash(transitionLeash, w, h));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                ViewUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(() -> ui.detachFromTransitionLeash(executor, onDone));
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index a5f8057..20efea5 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -28,11 +28,11 @@
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButtonColors
 import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
 
 @Composable
 fun PlatformButton(
@@ -100,12 +100,7 @@
     @DrawableRes iconResource: Int,
     contentDescription: String?,
 ) {
-    IconButton(
-        modifier = modifier,
-        onClick = onClick,
-        enabled = enabled,
-        colors = colors,
-    ) {
+    IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) {
         Icon(
             painter = painterResource(id = iconResource),
             contentDescription = contentDescription,
@@ -118,7 +113,7 @@
 
 @Composable
 private fun filledButtonColors(): ButtonColors {
-    val colors = LocalAndroidColorScheme.current
+    val colors = MaterialTheme.colorScheme
     return ButtonDefaults.buttonColors(
         containerColor = colors.primary,
         contentColor = colors.onPrimary,
@@ -127,27 +122,22 @@
 
 @Composable
 private fun outlineButtonColors(): ButtonColors {
-    return ButtonDefaults.outlinedButtonColors(
-        contentColor = LocalAndroidColorScheme.current.onSurface,
-    )
+    return ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface)
 }
 
 @Composable
 private fun iconButtonColors(): IconButtonColors {
     return IconButtonDefaults.filledIconButtonColors(
-        contentColor = LocalAndroidColorScheme.current.onSurface,
+        contentColor = MaterialTheme.colorScheme.onSurface
     )
 }
 
 @Composable
 private fun outlineButtonBorder(): BorderStroke {
-    return BorderStroke(
-        width = 1.dp,
-        color = LocalAndroidColorScheme.current.primary,
-    )
+    return BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primary)
 }
 
 @Composable
 private fun textButtonColors(): ButtonColors {
-    return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary)
+    return ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
index 0b96694..69ca0a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
 import kotlin.math.roundToInt
 
 /**
@@ -69,7 +68,7 @@
                 Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
                 propagateMinConstraints = true,
             ) {
-                val iconColor = LocalAndroidColorScheme.current.primary
+                val iconColor = MaterialTheme.colorScheme.primary
                 CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
             }
 
@@ -77,7 +76,7 @@
         }
 
         // Title.
-        val titleColor = LocalAndroidColorScheme.current.onSurface
+        val titleColor = MaterialTheme.colorScheme.onSurface
         CompositionLocalProvider(LocalContentColor provides titleColor) {
             ProvideTextStyle(
                 MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
@@ -88,7 +87,7 @@
         Spacer(Modifier.height(16.dp))
 
         // Content.
-        val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant
+        val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
         Box {
             CompositionLocalProvider(LocalContentColor provides contentColor) {
                 ProvideTextStyle(
@@ -169,7 +168,7 @@
                                 negative.width -
                                 positive.width -
                                 horizontalSpacing.roundToInt(),
-                            0
+                            0,
                         )
                     }
                 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index ba68917..29035ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -38,7 +38,6 @@
 import com.android.systemui.scene.data.repository.Idle
 import com.android.systemui.scene.data.repository.Transition
 import com.android.systemui.scene.data.repository.setSceneTransition
-import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
@@ -76,7 +75,6 @@
                 transitionInteractor = kosmos.keyguardTransitionInteractor,
                 dismissInteractor = dismissInteractor,
                 applicationScope = testScope.backgroundScope,
-                sceneInteractor = { kosmos.sceneInteractor },
                 deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor },
                 powerInteractor = kosmos.powerInteractor,
                 alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index 2580ac2..7798f46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.FLAG_QS_NEW_TILES;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
@@ -21,11 +23,16 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.service.quicksettings.Tile;
 import android.testing.UiThreadTest;
 import android.widget.ImageView;
@@ -47,7 +54,6 @@
 @UiThreadTest
 @SmallTest
 public class QSIconViewImplTest extends SysuiTestCase {
-
     private QSIconViewImpl mIconView;
 
     @Before
@@ -106,6 +112,34 @@
         verify(iv).setImageTintList(argThat(stateList -> stateList.getColors()[0] == desiredColor));
     }
 
+
+    @EnableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOn_immediatelyLoadsAll3TintColors() {
+        Context ctx = spy(mContext);
+
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+
+        iconView.getColor(new State()); // this should not increase the call count
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+    }
+
+    @DisableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOff_loadsOneTintColorAfterIconColorIsRead() {
+        Context ctx = spy(mContext);
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, never()).obtainStyledAttributes(any()); // none of the colors are preloaded
+
+        iconView.getColor(new State());
+
+        verify(ctx, times(1)).obtainStyledAttributes(any());
+    }
+
     @Test
     public void testStateSetCorrectly_toString() {
         ImageView iv = mock(ImageView.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
index 620e90d..d32ba47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -17,13 +17,17 @@
 package com.android.systemui.qs.tiles.impl.internet.domain
 
 import android.graphics.drawable.TestStubDrawable
+import android.os.fakeExecutorHandler
 import android.widget.Switch
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
@@ -31,6 +35,9 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,25 +46,93 @@
 class InternetTileMapperTest : SysuiTestCase() {
     private val kosmos = Kosmos()
     private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val handler = kosmos.fakeExecutorHandler
     private val mapper by lazy {
         InternetTileMapper(
             context.orCreateTestableResources
                 .apply {
                     addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+                    addOverride(R.drawable.ic_satellite_connected_2, TestStubDrawable())
                     addOverride(wifiRes, TestStubDrawable())
                 }
                 .resources,
             context.theme,
-            context
+            context,
+            handler,
         )
     }
 
     @Test
-    fun withActiveModel_mappedStateMatchesDataModel() {
+    fun withActiveCellularModel_mappedStateMatchesDataModel() {
         val inputModel =
             InternetTileModel.Active(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
-                iconId = wifiRes,
+                icon = InternetTileIconModel.Cellular(3),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_internet_label),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val signalDrawable = SignalDrawable(context, handler)
+        signalDrawable.setLevel(3)
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_networks_available),
+                Icon.Loaded(signalDrawable, null),
+                null,
+                context.getString(R.string.quick_settings_internet_label),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveSatelliteModel_mappedStateMatchesDataModel() {
+        val inputIcon =
+            SignalIconModel.Satellite(
+                3,
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_connected_2,
+                    contentDescription =
+                        ContentDescription.Resource(
+                            R.string.accessibility_status_bar_satellite_good_connection
+                        ),
+                ),
+            )
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.Satellite(inputIcon.icon),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(
+                        R.string.accessibility_status_bar_satellite_good_connection
+                    ),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedSatIcon = SatelliteIconModel.fromSignalStrength(3)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                inputModel.secondaryLabel.loadText(context).toString(),
+                Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null),
+                expectedSatIcon.res,
+                expectedSatIcon.contentDescription.loadContentDescription(context).toString(),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveWifiModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.ResourceId(wifiRes),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_internet_label),
@@ -71,7 +146,7 @@
                 context.getString(R.string.quick_settings_networks_available),
                 Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
                 wifiRes,
-                context.getString(R.string.quick_settings_internet_label)
+                context.getString(R.string.quick_settings_internet_label),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -81,7 +156,7 @@
         val inputModel =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
@@ -95,10 +170,10 @@
                 context.getString(R.string.quick_settings_networks_unavailable),
                 Icon.Loaded(
                     context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
-                    contentDescription = null
+                    contentDescription = null,
                 ),
                 R.drawable.ic_qs_no_internet_unavailable,
-                context.getString(R.string.quick_settings_networks_unavailable)
+                context.getString(R.string.quick_settings_networks_unavailable),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -107,7 +182,7 @@
         activationState: QSTileState.ActivationState,
         secondaryLabel: String,
         icon: Icon,
-        iconRes: Int,
+        iconRes: Int? = null,
         contentDescription: String,
     ): QSTileState {
         val label = context.getString(R.string.quick_settings_internet_label)
@@ -120,13 +195,13 @@
             setOf(
                 QSTileState.UserAction.CLICK,
                 QSTileState.UserAction.TOGGLE_CLICK,
-                QSTileState.UserAction.LONG_CLICK
+                QSTileState.UserAction.LONG_CLICK,
             ),
             contentDescription,
             null,
             QSTileState.SideViewIcon.Chevron,
             QSTileState.EnabledState.ENABLED,
-            Switch::class.qualifiedName
+            Switch::class.qualifiedName,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
index 5a45060..5259aa8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -18,14 +18,12 @@
 
 import android.graphics.drawable.TestStubDrawable
 import android.os.UserHandle
-import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.coroutines.collectLastValue
@@ -49,6 +47,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
@@ -60,9 +59,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -144,7 +141,6 @@
         underTest =
             InternetTileDataInteractor(
                 context,
-                testScope.coroutineContext,
                 testScope.backgroundScope,
                 airplaneModeRepository,
                 connectivityRepository,
@@ -164,9 +160,11 @@
 
             connectivityRepository.defaultConnections.value = DefaultConnectionModel()
 
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable)
             assertThat(latest?.secondaryLabel)
                 .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
         }
 
     @Test
@@ -183,11 +181,8 @@
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
 
-            val networkModel =
-                WifiNetworkModel.Active.of(
-                    level = 4,
-                    ssid = "test ssid",
-                )
+            val networkModel = WifiNetworkModel.Active.of(level = 4, ssid = "test ssid")
+
             val wifiIcon =
                 WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
                     as WifiIcon.Visible
@@ -198,12 +193,9 @@
 
             assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
             assertThat(latest?.secondaryLabel).isNull()
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
 
-            val actualIcon = latest?.icon
-            assertThat(actualIcon).isEqualTo(expectedIcon)
-            assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo("$internet,test ssid")
             val expectedSd = wifiIcon.contentDescription
@@ -229,8 +221,7 @@
             wifiRepository.setIsWifiDefault(true)
             wifiRepository.setWifiNetwork(networkModel)
 
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .doesNotContain(
@@ -249,9 +240,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_tablet
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -271,9 +261,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_laptop
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -293,10 +282,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_watch
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -315,10 +304,7 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
-                    null
-                )
+                InternetTileIconModel.ResourceId(com.android.settingslib.R.drawable.ic_hotspot_auto)
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -336,9 +322,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -358,9 +343,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -380,10 +364,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -426,8 +410,9 @@
             assertThat(latest?.secondaryLabel).isNull()
             assertThat(latest?.secondaryTitle)
                 .isEqualTo(context.getString(R.string.quick_settings_networks_available))
-            assertThat(latest?.icon).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_available)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             val expectedCd =
                 "$internet,${context.getString(R.string.quick_settings_networks_available)}"
@@ -435,54 +420,19 @@
                 .isEqualTo(expectedCd)
         }
 
-    /**
-     * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the
-     * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler()
-     * on the mentioned context. Since that context does not have a looper assigned to it, the
-     * handler instantiation will throw a RuntimeException.
-     *
-     * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So
-     *   either we should make Robolectric behave similar to the device test, or change this test to
-     *   look for a different signal than the exception, when run by Robolectric. For now we just
-     *   assume the test is not Robolectric.
-     */
-    @Test(expected = java.lang.RuntimeException::class)
-    fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() =
-        testScope.runTest {
-            assumeFalse(isRobolectricTest())
-
-            collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
-
-            connectivityRepository.setMobileConnected()
-            mobileConnectionsRepository.mobileIsDefault.value = true
-            mobileConnectionRepository.apply {
-                setAllLevels(3)
-                setAllRoaming(false)
-                networkName.value = NetworkNameModel.Default("test network")
-            }
-
-            runCurrent()
-        }
-
-    /**
-     * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the
-     * problem this test solves. The solution here is to assign a looper to the context via
-     * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for
-     * creating the SignalDrawable.
-     */
-    @TestableLooper.RunWithLooper
     @Test
-    fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() =
+    fun mobileDefault_usesNetworkNameAndIcon() =
         testScope.runTest {
             val latest by
                 collectLastValue(
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
+            val iconLevel = 3
 
             connectivityRepository.setMobileConnected()
             mobileConnectionsRepository.mobileIsDefault.value = true
             mobileConnectionRepository.apply {
-                setAllLevels(3)
+                setAllLevels(iconLevel)
                 setAllRoaming(false)
                 networkName.value = NetworkNameModel.Default("test network")
             }
@@ -491,8 +441,9 @@
             assertThat(latest?.secondaryTitle).isNotNull()
             assertThat(latest?.secondaryTitle.toString()).contains("test network")
             assertThat(latest?.secondaryLabel).isNull()
-            assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
-            assertThat(latest?.iconId).isNull()
+            val expectedIcon = InternetTileIconModel.Cellular(iconLevel)
+
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryTitle.toString())
             assertThat(latest?.contentDescription.loadContentDescription(context))
@@ -513,8 +464,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet_fully)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -534,8 +485,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -543,11 +494,7 @@
 
     private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
         val networkModel =
-            WifiNetworkModel.Active.of(
-                level = 4,
-                ssid = "test ssid",
-                hotspotDeviceType = hotspot,
-            )
+            WifiNetworkModel.Active.of(level = 4, ssid = "test ssid", hotspotDeviceType = hotspot)
 
         connectivityRepository.setWifiConnected()
         wifiRepository.setIsWifiDefault(true)
@@ -560,7 +507,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index 7b87aeb..d772e3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -42,7 +42,8 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.util.settings.FakeSettings
@@ -66,7 +67,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+@EnableFlags(NotificationMinimalism.FLAG_NAME)
 class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
 
     private val kosmos =
@@ -76,7 +77,7 @@
                 mock<SysuiStatusBarStateController>().also { mock ->
                     doAnswer { statusBarState.ordinal }.whenever(mock).state
                 }
-            fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+            lockScreenNotificationMinimalismSetting = true
         }
     private val notifPipeline: NotifPipeline = mock()
     private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
@@ -193,7 +194,7 @@
             kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
             assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(child2))
-                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+                .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
             assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
 
@@ -201,7 +202,7 @@
             kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
             assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
             assertThat(promoter.shouldPromoteToTopLevel(child2))
-                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+                .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
             assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
             assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
index 2159b86..ea2e25e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -57,7 +57,7 @@
     }
 
     @Test
-    @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+    @EnableFlags(NotificationMinimalism.FLAG_NAME)
     fun topOngoingAndUnseenNotification() = runTest {
         val entry1 = NotificationEntryBuilder().setTag("entry1").build()
         val entry2 = NotificationEntryBuilder().setTag("entry2").build()
@@ -91,4 +91,17 @@
         testScheduler.runCurrent()
         assertThat(settingEnabled).isTrue()
     }
+
+    fun testLockScreenNotificationMinimalismSetting() = runTest {
+        val settingEnabled by
+            collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled())
+
+        kosmos.lockScreenNotificationMinimalismSetting = false
+        testScheduler.runCurrent()
+        assertThat(settingEnabled).isFalse()
+
+        kosmos.lockScreenNotificationMinimalismSetting = true
+        testScheduler.runCurrent()
+        assertThat(settingEnabled).isTrue()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index f9b7769..28857a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -53,6 +54,7 @@
     private val testScope = kosmos.testScope
     private val zenModeRepository = kosmos.zenModeRepository
     private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+    private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository
 
     private val underTest = kosmos.emptyShadeViewModel
 
@@ -205,4 +207,84 @@
 
             assertThat(footerVisible).isTrue()
         }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY)
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setId("ID")
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+            assertThat(
+                    onClick?.targetIntent?.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)
+                )
+                .isEqualTo("ID")
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS)
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.onClick)
+            runCurrent()
+
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setActive(true)
+                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+                    .build()
+            )
+            runCurrent()
+
+            assertThat(onClick?.targetIntent?.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index be44dee..73626b4 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -184,7 +184,10 @@
             }
         }
 
-        /** Get the text for secondaryLabel. */
+        /**
+         *  If the current secondaryLabel value is not empty, ignore the given input and return
+         *  the current value. Otherwise return current value.
+         */
         public CharSequence getSecondaryLabel(CharSequence stateText) {
             // Use a local reference as the value might change from other threads
             CharSequence localSecondaryLabel = secondaryLabel;
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8f55961..0f1da50 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -70,6 +70,7 @@
         "jsr330",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "//frameworks/libs/systemui:msdl",
+        "//frameworks/libs/systemui:view_capture",
     ],
     resource_dirs: [
         "res",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index f358ba2..4db6ab6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.shared.rotation;
 
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
 import android.annotation.DimenRes;
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
@@ -30,7 +33,6 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
@@ -38,6 +40,7 @@
 import androidx.annotation.BoolRes;
 import androidx.core.view.OneShotPreDrawListener;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
 
 /**
@@ -47,7 +50,7 @@
 
     private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
 
-    private final WindowManager mWindowManager;
+    private final ViewCaptureAwareWindowManager mWindowManager;
     private final ViewGroup mKeyButtonContainer;
     private final FloatingRotationButtonView mKeyButtonView;
 
@@ -88,7 +91,8 @@
             @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
             @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
         mContext = context;
-        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+                enableViewCaptureTracing());
         mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
         mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
         mKeyButtonView.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 113e001..83f86a7 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,6 +65,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -499,8 +500,11 @@
 
         mDreamOverlayContainerViewController =
                 dreamOverlayComponent.getDreamOverlayContainerViewController();
-        mTouchMonitor = ambientTouchComponent.getTouchMonitor();
-        mTouchMonitor.init();
+
+        if (!SceneContainerFlag.isEnabled()) {
+            mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+            mTouchMonitor.init();
+        }
 
         mStateController.setShouldShowComplications(shouldShowComplications());
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 820c102..47f0ecf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -34,12 +34,14 @@
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.core.StatusBarSimpleFragment
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -57,7 +59,7 @@
         // Internal notification frontend dependencies
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
-        NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+        NotificationMinimalism.token dependsOn NotificationThrottleHun.token
         ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
         ModesEmptyShadeFix.token dependsOn modesUi
 
@@ -73,6 +75,8 @@
         // Status bar chip dependencies
         statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
         statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
+
+        StatusBarConnectedDisplays.token dependsOn StatusBarSimpleFragment.token
     }
 
     private inline val politeNotifications
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 68a252b..654c763 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -515,7 +515,13 @@
     }
 
     protected void notifyChange() {
-        mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */));
+        mBgHandler.post(() -> {
+            try {
+                mContentResolver.notifyChange(mSliceUri, null /* observer */);
+            } catch (Exception e) {
+                Log.e(TAG, "Error on mContentResolver.notifyChange()", e);
+            }
+        });
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index ea80911..18b1495 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.power.domain.interactor.PowerInteractor
-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.domain.interactor.ShadeInteractor
@@ -60,7 +59,6 @@
     transitionInteractor: KeyguardTransitionInteractor,
     val dismissInteractor: KeyguardDismissInteractor,
     @Application private val applicationScope: CoroutineScope,
-    sceneInteractor: Lazy<SceneInteractor>,
     deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
     powerInteractor: PowerInteractor,
     alternateBouncerInteractor: AlternateBouncerInteractor,
@@ -102,20 +100,20 @@
     private val isOnShadeWhileUnlocked: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
             combine(
-                    sceneInteractor.get().currentScene,
+                    shadeInteractor.get().isAnyExpanded,
                     deviceUnlockedInteractor.get().deviceUnlockStatus,
-                ) { scene, unlockStatus ->
-                    unlockStatus.isUnlocked &&
-                        (scene == Scenes.QuickSettings || scene == Scenes.Shade)
+                ) { isAnyExpanded, unlockStatus ->
+                    isAnyExpanded && unlockStatus.isUnlocked
                 }
                 .distinctUntilChanged()
         } else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
             combine(
-                shadeInteractor.get().isAnyExpanded,
-                keyguardInteractor.get().isKeyguardDismissible,
-            ) { isAnyExpanded, keyguardDismissible ->
-                isAnyExpanded && keyguardDismissible
-            }
+                    shadeInteractor.get().isAnyExpanded,
+                    keyguardInteractor.get().isKeyguardDismissible,
+                ) { isAnyExpanded, keyguardDismissible ->
+                    isAnyExpanded && keyguardDismissible
+                }
+                .distinctUntilChanged()
         } else {
             flow {
                 error(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 275f1ee..39cedc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -128,7 +128,7 @@
         data: MediaData,
         immediately: Boolean,
         receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
+        isSsReactivated: Boolean,
     ) {
         var reusedListener: PlaybackStateListener? = null
 
@@ -183,7 +183,7 @@
     override fun onSmartspaceMediaDataLoaded(
         key: String,
         data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
+        shouldPrioritize: Boolean,
     ) {
         if (!mediaFlags.isPersistentSsCardEnabled()) return
 
@@ -259,7 +259,9 @@
         }
 
         override fun onPlaybackStateChanged(state: PlaybackState?) {
-            processState(state, dispatchEvents = true, currentResumption = resumption)
+            bgExecutor.execute {
+                processState(state, dispatchEvents = true, currentResumption = resumption)
+            }
         }
 
         override fun onSessionDestroyed() {
@@ -276,6 +278,7 @@
             }
         }
 
+        @WorkerThread
         private fun processState(
             state: PlaybackState?,
             dispatchEvents: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 228b576..d413474 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -54,6 +54,7 @@
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
 import com.android.systemui.res.R
+import com.android.systemui.shared.system.ActivityManagerWrapper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.AsyncActivityLauncher
 import java.lang.IllegalArgumentException
@@ -62,9 +63,10 @@
 class MediaProjectionAppSelectorActivity(
     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
     private val activityLauncher: AsyncActivityLauncher,
+    private val activityManager: ActivityManagerWrapper,
     /** This is used to override the dependency in a screenshot test */
     @VisibleForTesting
-    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
+    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?,
 ) :
     ChooserActivity(),
     MediaProjectionAppSelectorView,
@@ -74,8 +76,9 @@
     @Inject
     constructor(
         componentFactory: MediaProjectionAppSelectorComponent.Factory,
-        activityLauncher: AsyncActivityLauncher
-    ) : this(componentFactory, activityLauncher, listControllerFactory = null)
+        activityLauncher: AsyncActivityLauncher,
+        activityManager: ActivityManagerWrapper,
+    ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null)
 
     private val lifecycleRegistry = LifecycleRegistry(this)
     override val lifecycle = lifecycleRegistry
@@ -100,7 +103,7 @@
                 callingPackage = callingPackage,
                 view = this,
                 resultHandler = this,
-                isFirstStart = savedInstanceState == null
+                isFirstStart = savedInstanceState == null,
             )
         component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
 
@@ -113,7 +116,7 @@
         intent.configureChooserIntent(
             resources,
             component.hostUserHandle,
-            component.personalProfileUserHandle
+            component.personalProfileUserHandle,
         )
 
         reviewGrantedConsentRequired =
@@ -180,7 +183,13 @@
         // is created and ready to be captured.
         val activityStarted =
             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
-                returnSelectedApp(launchCookie, taskId = -1)
+                if (targetInfo.resolvedComponentName == callingActivity) {
+                    // If attempting to launch the app used to launch the MediaProjection, then
+                    // provide the task id since the launch cookie won't match the existing task
+                    returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId)
+                } else {
+                    returnSelectedApp(launchCookie, taskId = -1)
+                }
             }
 
         // Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -213,7 +222,7 @@
             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                 RECORD_CANCEL,
                 reviewGrantedConsentRequired,
-                /* projection= */ null
+                /* projection= */ null,
             )
             if (isFinishing) {
                 // Only log dismissed when actually finishing, and not when changing configuration.
@@ -246,7 +255,7 @@
             val resultReceiver =
                 intent.getParcelableExtra(
                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
-                    ResultReceiver::class.java
+                    ResultReceiver::class.java,
                 ) as ResultReceiver
             val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId)
             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
@@ -260,8 +269,8 @@
             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
 
-            projection.setLaunchCookie(launchCookie)
-            projection.setTaskId(taskId)
+            projection.launchCookie = launchCookie
+            projection.taskId = taskId
 
             val intent = Intent()
             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
@@ -270,7 +279,7 @@
             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                 RECORD_CONTENT_TASK,
                 reviewGrantedConsentRequired,
-                projection
+                projection,
             )
         }
 
@@ -457,7 +466,7 @@
      */
     private class RecyclerViewExpandingAccessibilityDelegate(
         rdl: ResolverDrawerLayout,
-        view: RecyclerView
+        view: RecyclerView,
     ) : RecyclerViewAccessibilityDelegate(view) {
 
         private val delegate = AppListAccessibilityDelegate(rdl)
@@ -465,7 +474,7 @@
         override fun onRequestSendAccessibilityEvent(
             host: ViewGroup,
             child: View,
-            event: AccessibilityEvent
+            event: AccessibilityEvent,
         ): Boolean {
             super.onRequestSendAccessibilityEvent(host, child, event)
             return delegate.onRequestSendAccessibilityEvent(host, child, event)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 278352c..ead38f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.log.dagger.QSConfigLog
 import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.QSTile.State
 import com.android.systemui.statusbar.StatusBarState
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -57,6 +58,7 @@
     fun d(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
     }
+
     fun i(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
     }
@@ -73,7 +75,19 @@
                 str1 = tileSpec
                 str2 = reason
             },
-            { "[$str1] Tile destroyed. Reason: $str2" }
+            { "[$str1] Tile destroyed. Reason: $str2" },
+        )
+    }
+
+    fun logStateChanged(tileSpec: String, state: State) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                str2 = state.toString()
+            },
+            { "[$str1] Tile state=$str2" },
         )
     }
 
@@ -85,7 +99,7 @@
                 bool1 = listening
                 str1 = tileSpec
             },
-            { "[$str1] Tile listening=$bool1" }
+            { "[$str1] Tile listening=$bool1" },
         )
     }
 
@@ -98,7 +112,7 @@
                 str1 = containerName
                 str2 = allSpecs
             },
-            { "Tiles listening=$bool1 in $str1. $str2" }
+            { "Tiles listening=$bool1 in $str1. $str2" },
         )
     }
 
@@ -112,7 +126,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -124,7 +138,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling click." }
+            { "[$str1][$int1] Tile handling click." },
         )
     }
 
@@ -138,7 +152,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -150,7 +164,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling secondary click." }
+            { "[$str1][$int1] Tile handling secondary click." },
         )
     }
 
@@ -164,7 +178,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -176,7 +190,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling long click." }
+            { "[$str1][$int1] Tile handling long click." },
         )
     }
 
@@ -189,7 +203,7 @@
                 int1 = lastType
                 str2 = callback
             },
-            { "[$str1] mLastTileState=$int1, Callback=$str2." }
+            { "[$str1] mLastTileState=$int1, Callback=$str2." },
         )
     }
 
@@ -198,7 +212,7 @@
         tileSpec: String,
         state: Int,
         disabledByPolicy: Boolean,
-        color: Int
+        color: Int,
     ) {
         // This method is added to further debug b/250618218 which has only been observed from the
         // InternetTile, so we are only logging the background color change for the InternetTile
@@ -215,7 +229,7 @@
                 bool1 = disabledByPolicy
                 int2 = color
             },
-            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }
+            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." },
         )
     }
 
@@ -229,7 +243,7 @@
                 str3 = state.icon?.toString()
                 int1 = state.state
             },
-            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." }
+            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." },
         )
     }
 
@@ -241,7 +255,7 @@
                 str1 = containerName
                 bool1 = expanded
             },
-            { "$str1 expanded=$bool1" }
+            { "$str1 expanded=$bool1" },
         )
     }
 
@@ -253,7 +267,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewAttached: $str1 orientation $int1" }
+            { "onViewAttached: $str1 orientation $int1" },
         )
     }
 
@@ -265,7 +279,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewDetached: $str1 orientation $int1" }
+            { "onViewDetached: $str1 orientation $int1" },
         )
     }
 
@@ -276,7 +290,7 @@
         newShouldUseSplitShade: Boolean,
         oldScreenLayout: Int,
         newScreenLayout: Int,
-        containerName: String
+        containerName: String,
     ) {
         configChangedBuffer.log(
             TAG,
@@ -297,7 +311,7 @@
                     "screen layout=${toScreenLayoutString(long1.toInt())} " +
                     "(was ${toScreenLayoutString(long2.toInt())}), " +
                     "splitShade=$bool2 (was $bool1)"
-            }
+            },
         )
     }
 
@@ -305,7 +319,7 @@
         after: Boolean,
         before: Boolean,
         force: Boolean,
-        containerName: String
+        containerName: String,
     ) {
         buffer.log(
             TAG,
@@ -316,7 +330,7 @@
                 bool2 = before
                 bool3 = force
             },
-            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }
+            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" },
         )
     }
 
@@ -328,7 +342,7 @@
                 int1 = tilesPerPageCount
                 int2 = totalTilesCount
             },
-            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }
+            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" },
         )
     }
 
@@ -340,7 +354,7 @@
                 str1 = tileName
                 int1 = pageIndex
             },
-            { "Adding $str1 to page number $int1" }
+            { "Adding $str1 to page number $int1" },
         )
     }
 
@@ -361,7 +375,7 @@
                 str1 = viewName
                 str2 = toVisibilityString(visibility)
             },
-            { "$str1 visibility: $str2" }
+            { "$str1 visibility: $str2" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 5ea8c21..a4f3c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.qsNewTiles;
 import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
 
 import android.animation.Animator;
@@ -66,12 +67,22 @@
 
     private ValueAnimator mColorAnimator = new ValueAnimator();
 
+    private int mColorUnavailable;
+    private int mColorInactive;
+    private int mColorActive;
+
     public QSIconViewImpl(Context context) {
         super(context);
 
         final Resources res = context.getResources();
         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size);
 
+        if (qsNewTiles()) { // pre-load icon tint colors
+            mColorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline);
+            mColorInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
+            mColorActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
+        }
+
         mIcon = createIcon();
         addView(mIcon);
         mColorAnimator.setDuration(QS_ANIM_LENGTH);
@@ -195,7 +206,11 @@
     }
 
     protected int getColor(QSTile.State state) {
-        return getIconColorForState(getContext(), state);
+        if (qsNewTiles()) {
+            return getCachedIconColorForState(state);
+        } else {
+            return getIconColorForState(getContext(), state);
+        }
     }
 
     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
@@ -267,6 +282,19 @@
         }
     }
 
+    private int getCachedIconColorForState(QSTile.State state) {
+        if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
+            return mColorUnavailable;
+        } else if (state.state == Tile.STATE_INACTIVE) {
+            return mColorInactive;
+        } else if (state.state == Tile.STATE_ACTIVE) {
+            return mColorActive;
+        } else {
+            Log.e("QSIconView", "Invalid state " + state);
+            return 0;
+        }
+    }
+
     private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter {
         private Runnable mRunnable;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 4f3ea83..18b1f07 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -643,7 +643,6 @@
     }
 
     // HANDLE STATE CHANGES RELATED METHODS
-
     protected open fun handleStateChanged(state: QSTile.State) {
         val allowAnimations = animationsEnabled()
         isClickable = state.state != Tile.STATE_UNAVAILABLE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 5ea9e6a..301ab2b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -311,10 +311,7 @@
         mConfig = MobileMappings.Config.readConfig(mContext);
         mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
         mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
-        InternetTelephonyCallback telephonyCallback =
-                new InternetTelephonyCallback(mDefaultDataSubId);
-        mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback);
-        mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+        registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId);
         // Listen the connectivity changes
         mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback);
         mCanConfigWifi = canConfigWifi;
@@ -346,6 +343,23 @@
         mCallback = null;
     }
 
+    /**
+     * This is to generate and register the new callback to Telephony for uncached subscription id,
+     * then cache it. Telephony also cached this callback into
+     * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached
+     * already, it shall do nothing to avoid registering redundant callback to Telephony.
+     */
+    private void registerInternetTelephonyCallback(
+            TelephonyManager telephonyManager, int subId) {
+        if (mSubIdTelephonyCallbackMap.containsKey(subId)) {
+            // Avoid to generate and register unnecessary callback to Telephony.
+            return;
+        }
+        InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
+        mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+        telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+    }
+
     boolean isAirplaneModeEnabled() {
         return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
     }
@@ -673,9 +687,7 @@
             int subId = subInfo.getSubscriptionId();
             if (mSubIdTelephonyManagerMap.get(subId) == null) {
                 TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId);
-                InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
-                secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback);
-                mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+                registerInternetTelephonyCallback(secondaryTm, subId);
                 mSubIdTelephonyManagerMap.put(subId, secondaryTm);
             }
             return subId;
@@ -1351,6 +1363,7 @@
         if (DEBUG) {
             Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId);
         }
+
         if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) {
             // clean up old defaultDataSubId
             TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId);
@@ -1366,9 +1379,7 @@
             // create for new defaultDataSubId
             mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId);
             mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager);
-            InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId);
-            mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback);
-            mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback);
+            registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId);
             mCallback.onSubscriptionsChanged(defaultDataSubId);
         }
         mDefaultDataSubId = defaultDataSubId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
index 8965ef2..bb0b9b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import android.content.res.Resources
+import android.os.Handler
 import android.widget.Switch
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -28,6 +30,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import javax.inject.Inject
 
 /** Maps [InternetTileModel] to [QSTileState]. */
@@ -37,6 +40,7 @@
     @Main private val resources: Resources,
     private val theme: Resources.Theme,
     private val context: Context,
+    @Main private val handler: Handler,
 ) : QSTileDataToStateMapper<InternetTileModel> {
 
     override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
@@ -44,25 +48,42 @@
             label = resources.getString(R.string.quick_settings_internet_label)
             expandedAccessibilityClass = Switch::class
 
-            if (data.secondaryLabel != null) {
-                secondaryLabel = data.secondaryLabel.loadText(context)
-            } else {
-                secondaryLabel = data.secondaryTitle
-            }
+            secondaryLabel =
+                if (data.secondaryLabel != null) {
+                    data.secondaryLabel.loadText(context)
+                } else {
+                    data.secondaryTitle
+                }
 
             stateDescription = data.stateDescription.loadContentDescription(context)
             contentDescription = data.contentDescription.loadContentDescription(context)
 
-            iconRes = data.iconId
-            if (data.icon != null) {
-                this.icon = { data.icon }
-            } else if (data.iconId != null) {
-                val loadedIcon =
-                    Icon.Loaded(
-                        resources.getDrawable(data.iconId!!, theme),
-                        contentDescription = null
-                    )
-                this.icon = { loadedIcon }
+            when (val dataIcon = data.icon) {
+                is InternetTileIconModel.ResourceId -> {
+                    iconRes = dataIcon.resId
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resId, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
+
+                is InternetTileIconModel.Cellular -> {
+                    val signalDrawable = SignalDrawable(context, handler)
+                    signalDrawable.setLevel(dataIcon.level)
+                    icon = { Icon.Loaded(signalDrawable, contentDescription = null) }
+                }
+
+                is InternetTileIconModel.Satellite -> {
+                    iconRes = dataIcon.resourceIcon.res // level is inferred from res
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resourceIcon.res, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
             }
 
             sideViewIcon = QSTileState.SideViewIcon.Chevron
@@ -75,7 +96,7 @@
                 setOf(
                     QSTileState.UserAction.CLICK,
                     QSTileState.UserAction.TOGGLE_CLICK,
-                    QSTileState.UserAction.LONG_CLICK
+                    QSTileState.UserAction.LONG_CLICK,
                 )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
index 204ead3..6fe3979 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -20,13 +20,10 @@
 import android.content.Context
 import android.os.UserHandle
 import android.text.Html
-import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
@@ -36,12 +33,12 @@
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.utils.coroutines.flow.mapLatestConflated
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -51,7 +48,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
 /** Observes internet state changes providing the [InternetTileModel]. */
@@ -59,7 +55,6 @@
 @Inject
 constructor(
     private val context: Context,
-    @Main private val mainCoroutineContext: CoroutineContext,
     @Application private val scope: CoroutineScope,
     airplaneModeRepository: AirplaneModeRepository,
     private val connectivityRepository: ConnectivityRepository,
@@ -79,8 +74,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryTitle = secondary,
-                        iconId = wifiIcon.icon.res,
-                        icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+                        icon = InternetTileIconModel.ResourceId(wifiIcon.icon.res),
                         stateDescription = wifiIcon.contentDescription,
                         contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
                     )
@@ -116,11 +110,10 @@
             if (it == null) {
                 notConnectedFlow
             } else {
-                combine(
-                        it.networkName,
-                        it.signalLevelIcon,
-                        mobileDataContentName,
-                    ) { networkNameModel, signalIcon, dataContentDescription ->
+                combine(it.networkName, it.signalLevelIcon, mobileDataContentName) {
+                        networkNameModel,
+                        signalIcon,
+                        dataContentDescription ->
                         Triple(networkNameModel, signalIcon, dataContentDescription)
                     }
                     .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) ->
@@ -129,17 +122,12 @@
                                 val secondary =
                                     mobileDataContentConcat(
                                         networkNameModel.name,
-                                        dataContentDescription
+                                        dataContentDescription,
                                     )
 
-                                val drawable =
-                                    withContext(mainCoroutineContext) { SignalDrawable(context) }
-                                drawable.setLevel(signalIcon.level)
-                                val loadedIcon = Icon.Loaded(drawable, null)
-
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    icon = loadedIcon,
+                                    icon = InternetTileIconModel.Cellular(signalIcon.level),
                                     stateDescription =
                                         ContentDescription.Loaded(secondary.toString()),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
@@ -150,9 +138,10 @@
                                     signalIcon.icon.contentDescription.loadContentDescription(
                                         context
                                     )
+
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    iconId = signalIcon.icon.res,
+                                    icon = InternetTileIconModel.Satellite(signalIcon.icon),
                                     stateDescription = ContentDescription.Loaded(secondary),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
                                 )
@@ -164,7 +153,7 @@
 
     private fun mobileDataContentConcat(
         networkName: String?,
-        dataContentDescription: CharSequence?
+        dataContentDescription: CharSequence?,
     ): CharSequence {
         if (dataContentDescription == null) {
             return networkName ?: ""
@@ -177,9 +166,9 @@
             context.getString(
                 R.string.mobile_carrier_text_format,
                 networkName,
-                dataContentDescription
+                dataContentDescription,
             ),
-            0
+            0,
         )
     }
 
@@ -199,7 +188,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryLabel = secondary?.toText(),
-                        iconId = it.res,
+                        icon = InternetTileIconModel.ResourceId(it.res),
                         stateDescription = null,
                         contentDescription = secondary,
                     )
@@ -208,16 +197,18 @@
         }
 
     private val notConnectedFlow: StateFlow<InternetTileModel> =
-        combine(
-                wifiInteractor.areNetworksAvailable,
-                airplaneModeRepository.isAirplaneMode,
-            ) { networksAvailable, isAirplaneMode ->
+        combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) {
+                networksAvailable,
+                isAirplaneMode ->
                 when {
                     isAirplaneMode -> {
                         val secondary = context.getString(R.string.status_bar_airplane)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_unavailable,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_unavailable
+                                ),
                             stateDescription = null,
                             contentDescription = ContentDescription.Loaded(secondary),
                         )
@@ -227,10 +218,13 @@
                             context.getString(R.string.quick_settings_networks_available)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_available,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_available
+                                ),
                             stateDescription = null,
                             contentDescription =
-                                ContentDescription.Loaded("$internetLabel,$secondary")
+                                ContentDescription.Loaded("$internetLabel,$secondary"),
                         )
                     }
                     else -> {
@@ -248,7 +242,7 @@
      */
     override fun tileData(
         user: UserHandle,
-        triggers: Flow<DataUpdateTrigger>
+        triggers: Flow<DataUpdateTrigger>,
     ): Flow<InternetTileModel> =
         connectivityRepository.defaultConnections.flatMapLatest {
             when {
@@ -265,7 +259,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
index ece90461..15b4e47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
@@ -17,23 +17,21 @@
 package com.android.systemui.qs.tiles.impl.internet.domain.model
 
 import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 
 /** Model describing the state that the QS Internet tile should be in. */
 sealed interface InternetTileModel {
     val secondaryTitle: CharSequence?
     val secondaryLabel: Text?
-    val iconId: Int?
-    val icon: Icon?
+    val icon: InternetTileIconModel
     val stateDescription: ContentDescription?
     val contentDescription: ContentDescription?
 
     data class Active(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
@@ -41,8 +39,7 @@
     data class Inactive(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
similarity index 66%
copy from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
index 06f3db5..54a18f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
@@ -14,39 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.shared
+package com.android.systemui.statusbar.core
 
-import android.os.SystemProperties
 import com.android.systemui.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the minimalism prototype flag state. */
+/** Helper for reading or using the status bar connected displays flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object StatusBarConnectedDisplays {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS
 
     /** A token used for dependency declaration */
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the heads-up cycling animation enabled */
+    /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationMinimalismPrototype()
-
-    /**
-     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
-     * not grouped, but this property read allows that behavior to be disabled.
-     */
-    val ungroupTopUnseen: Boolean
-        get() =
-            if (isUnexpectedlyInLegacyMode()) false
-            else
-                SystemProperties.getBoolean(
-                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                    false
-                )
+        get() = Flags.statusBarConnectedDisplays()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
@@ -58,6 +45,14 @@
         RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
 
     /**
+     * Called to ensure code is only run when the flag is enabled. This will throw an exception if
+     * the flag is not enabled to ensure that the refactor author catches issues in testing.
+     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+    /**
      * Called to ensure code is only run when the flag is disabled. This will throw an exception if
      * the flag is enabled to ensure that the refactor author catches issues in testing.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index 0f93b5d..231a0b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -16,6 +16,11 @@
 package com.android.systemui.statusbar.notification
 
 import android.content.Intent
+import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
+import android.provider.Settings.ACTION_NOTIFICATION_HISTORY
+import android.provider.Settings.ACTION_NOTIFICATION_SETTINGS
+import android.provider.Settings.ACTION_ZEN_MODE_SETTINGS
+import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
 import android.view.View
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
@@ -25,6 +30,7 @@
  * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
  */
 interface NotificationActivityStarter {
+
     /** Called when the user clicks on the notification bubble icon. */
     fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
 
@@ -35,14 +41,63 @@
     fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
 
     /**
-     * Called when the user clicks "Manage" or "History" in the Shade, or the "No notifications"
-     * text.
+     * Called when the user clicks "Manage" or "History" in the Shade. Prefer using
+     * [startSettingsIntent] instead.
      */
     fun startHistoryIntent(view: View?, showHistory: Boolean)
 
+    /**
+     * Called to open a settings intent from a launchable view (such as the "Manage" or "History"
+     * button in the shade, or the "No notifications" text).
+     *
+     * @param view the view to perform the launch animation from (must extend [LaunchableView])
+     * @param intentInfo information about the (settings) intent to be launched
+     */
+    fun startSettingsIntent(view: View, intentInfo: SettingsIntent)
+
     /** Called when the user succeed to drop notification to proper target view. */
     fun onDragSuccess(entry: NotificationEntry?)
 
     val isCollapsingToShowActivityOverLockscreen: Boolean
         get() = false
+
+    /**
+     * Information about a settings intent to be launched.
+     *
+     * If the [targetIntent] is T and [backStack] is [A, B, C], the stack will look like
+     * [A, B, C, T].
+     */
+    data class SettingsIntent(
+        var targetIntent: Intent,
+        var backStack: List<Intent> = emptyList(),
+        var cujType: Int? = null,
+    ) {
+        // Utility factory methods for known intents
+        companion object {
+            fun forNotificationSettings(cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent = Intent(ACTION_NOTIFICATION_SETTINGS),
+                    cujType = cujType,
+                )
+
+            fun forNotificationHistory(cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent = Intent(ACTION_NOTIFICATION_HISTORY),
+                    backStack = listOf(Intent(ACTION_NOTIFICATION_SETTINGS)),
+                    cujType = cujType,
+                )
+
+            fun forModesSettings(cujType: Int? = null) =
+                SettingsIntent(targetIntent = Intent(ACTION_ZEN_MODE_SETTINGS), cujType = cujType)
+
+            fun forModeSettings(modeId: String, cujType: Int? = null) =
+                SettingsIntent(
+                    targetIntent =
+                        Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+                            .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId),
+                    backStack = listOf(Intent(ACTION_ZEN_MODE_SETTINGS)),
+                    cujType = cujType,
+                )
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index b342722..b67092c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -22,7 +22,7 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE
@@ -54,7 +54,7 @@
     fun getNotificationBuckets(): IntArray {
         if (
             PriorityPeopleSection.isEnabled ||
-                NotificationMinimalismPrototype.isEnabled ||
+                NotificationMinimalism.isEnabled ||
                 NotificationClassificationFlag.isEnabled
         ) {
             // We don't need this list to be adaptive, it can be the superset of all features.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index a621b2a..4e63b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
 import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
 import com.android.systemui.util.asIndenting
@@ -77,7 +77,7 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) {
             return
         }
         pipeline.addPromoter(unseenNotifPromoter)
@@ -129,26 +129,25 @@
             }
     }
 
-    private fun unseenFeatureEnabled(): Flow<Boolean> {
-        // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
-        //  Or should we actually just repurpose using the existing setting?
-        if (NotificationMinimalismPrototype.isEnabled) {
-            return flowOf(true)
+    private fun minimalismFeatureSettingEnabled(): Flow<Boolean> {
+        if (!NotificationMinimalism.isEnabled) {
+            return flowOf(false)
         }
-        return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
+        return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled()
     }
 
     private suspend fun trackUnseenFilterSettingChanges() {
-        unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+        // Only filter the seen notifs when the lock screen minimalism feature settings is on.
+        minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled ->
             // update local field and invalidate if necessary
-            if (isSettingEnabled != unseenFilterEnabled) {
-                unseenFilterEnabled = isSettingEnabled
+            if (isMinimalismSettingEnabled != unseenFilterEnabled) {
+                unseenFilterEnabled = isMinimalismSettingEnabled
                 unseenNotifications.clear()
                 unseenNotifPromoter.invalidateList("unseen setting changed")
             }
             // if the setting is enabled, then start tracking and filtering unseen notifications
-            logger.logTrackingUnseen(isSettingEnabled)
-            if (isSettingEnabled) {
+            logger.logTrackingUnseen(isMinimalismSettingEnabled)
+            if (isMinimalismSettingEnabled) {
                 trackSeenNotifications()
             }
         }
@@ -178,7 +177,7 @@
         }
 
     private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         if (!unseenFilterEnabled) return
         // Only ever elevate a top unseen notification on keyguard, not even locked shade
         if (statusBarStateController.state != StatusBarState.KEYGUARD) {
@@ -215,9 +214,9 @@
         object : NotifPromoter(TAG) {
             override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
                 when {
-                    NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+                    NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false
                     seenNotificationsInteractor.isTopOngoingNotification(child) -> true
-                    !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+                    !NotificationMinimalism.ungroupTopUnseen -> false
                     else -> seenNotificationsInteractor.isTopUnseenNotification(child)
                 }
         }
@@ -225,7 +224,7 @@
     val topOngoingSectioner =
         object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
             override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
                 }
@@ -235,7 +234,7 @@
     val topUnseenSectioner =
         object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
             override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
index e44a77c..a4fa729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -34,7 +34,10 @@
             TAG,
             LogLevel.DEBUG,
             messageInitializer = { bool1 = trackingUnseen },
-            messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+            messagePrinter = {
+                "${if (bool1) "Start" else "Stop"} " +
+                    "tracking unseen notifications because of settings change."
+            },
         )
 
     fun logShadeVisible(numUnseen: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 73ce48b..96c260b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -88,11 +88,10 @@
         mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mCoordinators.add(lockScreenMinimalismCoordinator)
-        } else {
-            mCoordinators.add(unseenKeyguardCoordinator)
         }
+        mCoordinators.add(unseenKeyguardCoordinator)
         mCoordinators.add(rankingCoordinator)
         mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
@@ -125,11 +124,11 @@
         }
 
         // Manually add Ordered Sections
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
         }
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        if (NotificationMinimalismPrototype.isEnabled) {
+        if (NotificationMinimalism.isEnabled) {
             mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
         }
         mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index bfea2ba..cf1329c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.asIndenting
@@ -51,7 +50,6 @@
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -87,7 +85,6 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        NotificationMinimalismPrototype.assertInLegacyMode()
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
         scope.launch { trackUnseenFilterSettingChanges() }
@@ -253,10 +250,6 @@
     }
 
     private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (NotificationMinimalismPrototype.isEnabled) {
-            // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
-            return flowOf(false)
-        }
         return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index 5ff5d2d..1fe32c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -117,18 +117,12 @@
                             (entry.getSbn().getNotification().flags and
                                 FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
                         ) {
+                            // If we've received an update from the system and the entry is marked
+                            // as lifetime extended, that means system server has received a
+                            // cancelation in response to a direct reply, and sent an update to
+                            // let system ui know that it should rebuild the notification with
+                            // that direct reply.
                             if (
-                                mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
-                                    entry
-                                )
-                            ) {
-                                val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
-                                entry.onRemoteInputInserted()
-                                mNotifUpdater.onInternalNotificationUpdate(
-                                    newSbn,
-                                    "Extending lifetime of notification with remote input",
-                                )
-                            } else if (
                                 mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
                                     entry
                                 )
@@ -140,16 +134,11 @@
                                     "Extending lifetime of notification with smart reply",
                                 )
                             } else {
-                                // The app may have re-cancelled a notification after it had already
-                                // been lifetime extended.
-                                // Rebuild the notification with the replies it already had to
-                                // ensure
-                                // those replies continue to be displayed.
-                                val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
+                                val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+                                entry.onRemoteInputInserted()
                                 mNotifUpdater.onInternalNotificationUpdate(
                                     newSbn,
-                                    "Extending lifetime of notification that has already been " +
-                                        "lifetime extended.",
+                                    "Extending lifetime of notification with remote input",
                                 )
                             }
                         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 6d0148a..41419f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -41,7 +41,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.kotlin.BooleanFlowOperators;
@@ -170,7 +170,7 @@
                     if (entry == null) {
                         return false;
                     }
-                    boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+                    boolean isTopUnseen = NotificationMinimalism.isEnabled()
                             && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
                                 || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
                     if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 2956432..1babe47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.util.printSection
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -57,25 +57,25 @@
 
     /** Set the entry that is identified as the top ongoing notification. */
     fun setTopOngoingNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topOngoingNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top ongoing notification. */
     fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
         else
             entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
 
     /** Set the entry that is identified as the top unseen notification. */
     fun setTopUnseenNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topUnseenNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top unseen notification. */
     fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
         else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
 
     fun dump(pw: IndentingPrintWriter) =
@@ -120,4 +120,29 @@
             // only track the most recent emission, if events are happening faster than they can be
             // consumed
             .conflate()
+
+    fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> =
+        secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+                    default = 1,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
index 102a11c..7f1b043 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder
 
 import android.view.View
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
 import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
 import kotlinx.coroutines.coroutineScope
@@ -26,18 +27,16 @@
     suspend fun bind(
         view: EmptyShadeView,
         viewModel: EmptyShadeViewModel,
-        launchNotificationSettings: View.OnClickListener,
-        launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ) = coroutineScope {
         launch { viewModel.text.collect { view.setText(it) } }
 
         launch {
-            viewModel.tappingShouldLaunchHistory.collect { shouldLaunchHistory ->
-                if (shouldLaunchHistory) {
-                    view.setOnClickListener(launchNotificationHistory)
-                } else {
-                    view.setOnClickListener(launchNotificationSettings)
+            viewModel.onClick.collect { settingsIntent ->
+                val onClickListener = { view: View ->
+                    notificationActivityStarter.startSettingsIntent(view, settingsIntent)
                 }
+                view.setOnClickListener(onClickListener)
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
index d5417e7..8c8f200 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.modes.shared.ModesUi
 import com.android.systemui.res.R
 import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -34,6 +35,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
@@ -80,8 +82,7 @@
             if (ModesUi.isEnabled) {
                 zenModeInteractor.modesHidingNotifications.map { modes ->
                     // Create a string that is either "No notifications" if no modes are filtering
-                    // them
-                    // out, or something like "Notifications paused by SomeMode" otherwise.
+                    // them out, or something like "Notifications paused by SomeMode" otherwise.
                     val msgFormat =
                         MessageFormat(
                             context.getString(R.string.modes_suppressing_shade_text),
@@ -116,9 +117,26 @@
         )
     }
 
-    val tappingShouldLaunchHistory by lazy {
+    val onClick: Flow<SettingsIntent> by lazy {
         ModesEmptyShadeFix.assertInNewMode()
-        notificationSettingsInteractor.isNotificationHistoryEnabled
+        combine(
+            zenModeInteractor.modesHidingNotifications,
+            notificationSettingsInteractor.isNotificationHistoryEnabled,
+        ) { modes, isNotificationHistoryEnabled ->
+            if (modes.isNotEmpty()) {
+                if (modes.size == 1) {
+                    SettingsIntent.forModeSettings(modes[0].id)
+                } else {
+                    SettingsIntent.forModesSettings()
+                }
+            } else {
+                if (isNotificationHistoryEnabled) {
+                    SettingsIntent.forNotificationHistory()
+                } else {
+                    SettingsIntent.forNotificationSettings()
+                }
+            }
+        }
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 920541d..22bec5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -19,6 +19,8 @@
 import android.view.View
 import androidx.lifecycle.lifecycleScope
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
 import com.android.systemui.util.ui.isAnimating
@@ -36,6 +38,7 @@
         clearAllNotifications: View.OnClickListener,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ): DisposableHandle {
         return footer.repeatWhenAttached {
             lifecycleScope.launch {
@@ -45,6 +48,7 @@
                     clearAllNotifications,
                     launchNotificationSettings,
                     launchNotificationHistory,
+                    notificationActivityStarter,
                 )
             }
         }
@@ -56,6 +60,7 @@
         clearAllNotifications: View.OnClickListener,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ) = coroutineScope {
         launch { bindClearAllButton(footer, viewModel, clearAllNotifications) }
         launch {
@@ -64,6 +69,7 @@
                 viewModel,
                 launchNotificationSettings,
                 launchNotificationHistory,
+                notificationActivityStarter,
             )
         }
         launch { bindMessage(footer, viewModel) }
@@ -113,13 +119,23 @@
         viewModel: FooterViewModel,
         launchNotificationSettings: View.OnClickListener,
         launchNotificationHistory: View.OnClickListener,
+        notificationActivityStarter: NotificationActivityStarter,
     ) = coroutineScope {
         launch {
-            viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
-                if (shouldLaunchHistory) {
-                    footer.setManageButtonClickListener(launchNotificationHistory)
-                } else {
-                    footer.setManageButtonClickListener(launchNotificationSettings)
+            if (ModesEmptyShadeFix.isEnabled) {
+                viewModel.manageOrHistoryButtonClick.collect { settingsIntent ->
+                    val onClickListener = { view: View ->
+                        notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+                    }
+                    footer.setManageButtonClickListener(onClickListener)
+                }
+            } else {
+                viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
+                    if (shouldLaunchHistory) {
+                        footer.setManageButtonClickListener(launchNotificationHistory)
+                    } else {
+                        footer.setManageButtonClickListener(launchNotificationSettings)
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 90fb728..a3f4cd2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -16,12 +16,17 @@
 
 package com.android.systemui.statusbar.notification.footer.ui.viewmodel
 
+import android.content.Intent
+import android.provider.Settings
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.util.kotlin.sample
@@ -80,7 +85,7 @@
                         combine(
                                 shadeInteractor.isShadeFullyExpanded,
                                 shadeInteractor.isShadeTouchable,
-                                ::Pair
+                                ::Pair,
                             )
                             .onStart { emit(Pair(false, false)) }
                     ) { clearAllButtonVisible, (isShadeFullyExpanded, animationsEnabled) ->
@@ -93,8 +98,28 @@
     val manageButtonShouldLaunchHistory =
         notificationSettingsInteractor.isNotificationHistoryEnabled
 
+    // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel.
+    val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy {
+        if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+            flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS)))
+        } else {
+            notificationSettingsInteractor.isNotificationHistoryEnabled.map {
+                isNotificationHistoryEnabled ->
+                if (isNotificationHistoryEnabled) {
+                    SettingsIntent.forNotificationHistory(
+                        cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+                    )
+                } else {
+                    SettingsIntent.forNotificationSettings(
+                        cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+                    )
+                }
+            }
+        }
+    }
+
     private val manageOrHistoryButtonText: Flow<Int> =
-        manageButtonShouldLaunchHistory.map { shouldLaunchHistory ->
+        notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory ->
             if (shouldLaunchHistory) R.string.manage_notifications_history_text
             else R.string.manage_notifications_text
         }
@@ -128,7 +153,7 @@
                     activeNotificationsInteractor.get(),
                     notificationSettingsInteractor.get(),
                     seenNotificationsInteractor.get(),
-                    shadeInteractor.get()
+                    shadeInteractor.get(),
                 )
             )
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
index 06f3db5..70bb272 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
@@ -17,23 +17,23 @@
 package com.android.systemui.statusbar.notification.shared
 
 import android.os.SystemProperties
-import com.android.systemui.Flags
+import com.android.server.notification.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the minimalism prototype flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object NotificationMinimalism {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM
 
     /** A token used for dependency declaration */
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the heads-up cycling animation enabled */
+    /** Is the notification minimalism enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationMinimalismPrototype()
+        get() = Flags.notificationMinimalism()
 
     /**
      * The prototype will (by default) use a promoter to ensure that the top unseen notification is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index d246b04..129d4ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -224,6 +224,7 @@
      * @param isSwipingUp Whether we are swiping up.
      */
     public void setSwipingUp(boolean isSwipingUp) {
+        SceneContainerFlag.assertInLegacyMode();
         if (!isSwipingUp && mIsSwipingUp) {
             // Just stopped swiping up.
             mIsFlingRequiredAfterLockScreenSwipeUp = true;
@@ -242,6 +243,7 @@
      * @param isFlinging Whether we are flinging the shade open or closed.
      */
     public void setFlinging(boolean isFlinging) {
+        SceneContainerFlag.assertInLegacyMode();
         if (isOnKeyguard() && !isFlinging && mIsFlinging) {
             // Just stopped flinging.
             mIsFlingRequiredAfterLockScreenSwipeUp = false;
@@ -717,6 +719,7 @@
      * @return Whether we need to do a fling down after swiping up on lockscreen.
      */
     public boolean isFlingingAfterSwipeUpOnLockscreen() {
+        SceneContainerFlag.assertInLegacyMode();
         return mIsFlinging && mIsFlingRequiredAfterLockScreenSwipeUp;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0a44a2b..b466bf0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -568,6 +568,7 @@
     private boolean mHasFilteredOutSeenNotifications;
     @Nullable private SplitShadeStateController mSplitShadeStateController = null;
     private boolean mIsSmallLandscapeLockscreenEnabled = false;
+    private boolean mSuppressHeightUpdates;
 
     /** Pass splitShadeStateController to view and update split shade */
     public void passSplitShadeStateController(SplitShadeStateController splitShadeStateController) {
@@ -1458,9 +1459,13 @@
      * 2) Swiping up on lockscreen or flinging down after swipe up
      */
     private boolean shouldSkipHeightUpdate() {
-        return mAmbientState.isOnKeyguard()
-                && (mAmbientState.isSwipingUp()
-                || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+        if (SceneContainerFlag.isEnabled()) {
+            return mSuppressHeightUpdates;
+        } else {
+            return mAmbientState.isOnKeyguard()
+                    && (mAmbientState.isSwipingUp()
+                    || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+        }
     }
 
     /**
@@ -5399,6 +5404,7 @@
     }
 
     public void setPanelFlinging(boolean flinging) {
+        SceneContainerFlag.assertInLegacyMode();
         mAmbientState.setFlinging(flinging);
         if (!flinging) {
             // re-calculate the stack height which was frozen while flinging
@@ -5406,6 +5412,12 @@
         }
     }
 
+    @Override
+    public void suppressHeightUpdates(boolean suppress) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mSuppressHeightUpdates = suppress;
+    }
+
     public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
         mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 9c5fecf..7b02d0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1439,6 +1439,7 @@
     }
 
     public void setPanelFlinging(boolean flinging) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setPanelFlinging(flinging);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 06222fd..3bc5495 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.Compile
 import com.android.systemui.util.children
@@ -74,7 +74,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalismPrototype.isEnabled
+        get() = NotificationMinimalism.isEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -406,7 +406,7 @@
     fun updateResources() {
         maxKeyguardNotifications =
             infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
-        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
+        maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 0113e36..dbe81c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -124,4 +124,7 @@
 
     /** @see addHeadsUpHeightChangedListener */
     fun removeHeadsUpHeightChangedListener(runnable: Runnable)
+
+    /** Sets whether updates to the stack are are suppressed. */
+    fun suppressHeightUpdates(suppress: Boolean)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 3dad326..ebae235 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -187,6 +187,7 @@
                 },
                 launchNotificationSettings,
                 launchNotificationHistory,
+                notificationActivityStarter.get(),
             )
         if (SceneContainerFlag.isEnabled) {
             launch {
@@ -266,8 +267,7 @@
                 EmptyShadeViewBinder.bind(
                     emptyShadeView,
                     emptyShadeViewModel,
-                    launchNotificationSettings,
-                    launchNotificationHistory,
+                    notificationActivityStarter.get(),
                 )
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 99ff678..87d70ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -111,6 +111,7 @@
             launch {
                 viewModel.shouldCloseGuts.filter { it }.collect { view.closeGutsOnSceneTouch() }
             }
+            launch { viewModel.suppressHeightUpdates.collect { view.suppressHeightUpdates(it) } }
 
             launchAndDispose {
                 view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index cd9c07e..c9eaec7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.ObservableTransitionState.Idle
 import com.android.compose.animation.scene.ObservableTransitionState.Transition
 import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene
@@ -48,6 +49,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@@ -129,6 +131,14 @@
         }
     }
 
+    /** Are notification stack height updates suppressed? */
+    val suppressHeightUpdates: Flow<Boolean> =
+        sceneInteractor.transitionState.map { transition: ObservableTransitionState ->
+            transition is Transition &&
+                transition.fromContent == Scenes.Lockscreen &&
+                (transition.toContent == Scenes.Bouncer || transition.toContent == Scenes.Gone)
+        }
+
     /**
      * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
      * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 0a6e7f5..ee96195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -43,6 +43,7 @@
 import android.util.EventLog;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -74,6 +75,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
@@ -249,7 +251,7 @@
      * Called when a notification is clicked.
      *
      * @param entry notification that was clicked
-     * @param row row for that notification
+     * @param row   row for that notification
      */
     @Override
     public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
@@ -547,8 +549,8 @@
                             (adapter) -> TaskStackBuilder.create(mContext)
                                     .addNextIntentWithParentStack(intent)
                                     .startActivities(getActivityOptions(
-                                            mDisplayId,
-                                            adapter),
+                                                    mDisplayId,
+                                                    adapter),
                                             new UserHandle(UserHandle.getUserId(appUid))));
                 });
                 return true;
@@ -565,6 +567,7 @@
 
     @Override
     public void startHistoryIntent(View view, boolean showHistory) {
+        ModesEmptyShadeFix.assertInLegacyMode();
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
@@ -585,14 +588,14 @@
                             );
                     ActivityTransitionAnimator.Controller animationController =
                             viewController == null ? null
-                                : new StatusBarTransitionAnimatorController(
-                                        viewController,
-                                        mShadeAnimationInteractor,
-                                        mShadeController,
-                                        mNotificationShadeWindowController,
-                                        mCommandQueue,
-                                        mDisplayId,
-                                        true /* isActivityIntent */);
+                                    : new StatusBarTransitionAnimatorController(
+                                            viewController,
+                                            mShadeAnimationInteractor,
+                                            mShadeController,
+                                            mNotificationShadeWindowController,
+                                            mCommandQueue,
+                                            mDisplayId,
+                                            true /* isActivityIntent */);
 
                     mActivityTransitionAnimator.startIntentWithAnimation(
                             animationController, animate, intent.getPackage(),
@@ -612,6 +615,51 @@
                 false /* afterKeyguardGone */);
     }
 
+    @Override
+    public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) {
+        boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
+        ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
+            @Override
+            public boolean onDismiss() {
+                AsyncTask.execute(() -> {
+                    TaskStackBuilder tsb = TaskStackBuilder.create(mContext);
+                    for (Intent intent : intentInfo.getBackStack()) {
+                        tsb.addNextIntent(intent);
+                    }
+                    tsb.addNextIntent(intentInfo.getTargetIntent());
+
+                    ActivityTransitionAnimator.Controller viewController =
+                            ActivityTransitionAnimator.Controller.fromView(view,
+                                    intentInfo.getCujType());
+                    ActivityTransitionAnimator.Controller animationController =
+                            viewController == null ? null
+                                    : new StatusBarTransitionAnimatorController(
+                                            viewController,
+                                            mShadeAnimationInteractor,
+                                            mShadeController,
+                                            mNotificationShadeWindowController,
+                                            mCommandQueue,
+                                            mDisplayId,
+                                            true /* isActivityIntent */);
+
+                    mActivityTransitionAnimator.startIntentWithAnimation(
+                            animationController, animate, intentInfo.getTargetIntent().getPackage(),
+                            (adapter) -> tsb.startActivities(
+                                    getActivityOptions(mDisplayId, adapter),
+                                    mUserTracker.getUserHandle()));
+                });
+                return true;
+            }
+
+            @Override
+            public boolean willRunAnimationOnKeyguard() {
+                return animate;
+            }
+        };
+        mActivityStarter.dismissKeyguardThenExecute(onDismissAction, null,
+                false /* afterKeyguardGone */);
+    }
+
     private void removeHunAfterClick(ExpandableNotificationRow row) {
         String key = row.getEntry().getSbn().getKey();
         if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt
new file mode 100644
index 0000000..f8958e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+
+sealed interface InternetTileIconModel {
+    data class ResourceId(val resId: Int) : InternetTileIconModel
+
+    data class Cellular(val level: Int) : InternetTileIconModel
+
+    data class Satellite(val resourceIcon: Icon.Resource) : InternetTileIconModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index e1dcc52..78edd39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -19,8 +19,10 @@
 import android.content.Intent
 import android.provider.Settings
 import android.util.Log
+import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
@@ -30,6 +32,7 @@
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.PlatformButton
 import com.android.compose.PlatformOutlinedButton
+import com.android.compose.theme.PlatformTheme
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
@@ -74,7 +77,7 @@
             currentDialog?.dismiss()
         }
 
-        currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) }
+        currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) }
         currentDialog
             ?.lifecycle
             ?.addObserver(
@@ -91,28 +94,34 @@
 
     @Composable
     private fun ModesDialogContent(dialog: SystemUIDialog) {
-        AlertDialogContent(
-            modifier = Modifier.semantics {
-                testTagsAsResourceId = true
-            },
-            title = {
-                Text(
-                    modifier = Modifier.testTag("modes_title"),
-                    text = stringResource(R.string.zen_modes_dialog_title)
-                )
-            },
-            content = { ModeTileGrid(viewModel.get()) },
-            neutralButton = {
-                PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
-                    Text(stringResource(R.string.zen_modes_dialog_settings))
-                }
-            },
-            positiveButton = {
-                PlatformButton(onClick = { dialog.dismiss() }) {
-                    Text(stringResource(R.string.zen_modes_dialog_done))
-                }
-            },
-        )
+        // TODO(b/369376884): The composable does correctly update when the theme changes
+        //  while the dialog is open, but the background (which we don't control here)
+        //  doesn't, which causes us to show things like white text on a white background.
+        //  as a workaround, we remember the original theme and keep it on recomposition.
+        val isCurrentlyInDarkTheme = isSystemInDarkTheme()
+        val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
+        PlatformTheme(isDarkTheme = cachedDarkTheme) {
+            AlertDialogContent(
+                modifier = Modifier.semantics { testTagsAsResourceId = true },
+                title = {
+                    Text(
+                        modifier = Modifier.testTag("modes_title"),
+                        text = stringResource(R.string.zen_modes_dialog_title),
+                    )
+                },
+                content = { ModeTileGrid(viewModel.get()) },
+                neutralButton = {
+                    PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
+                        Text(stringResource(R.string.zen_modes_dialog_settings))
+                    }
+                },
+                positiveButton = {
+                    PlatformButton(onClick = { dialog.dismiss() }) {
+                        Text(stringResource(R.string.zen_modes_dialog_done))
+                    }
+                },
+            )
+        }
     }
 
     @VisibleForTesting
@@ -129,7 +138,7 @@
         activityStarter.startActivity(
             ZEN_MODE_SETTINGS_INTENT,
             true /* dismissShade */,
-            animationController
+            animationController,
         )
     }
 
@@ -163,7 +172,7 @@
             Log.w(
                 TAG,
                 "Cannot launch from dialog, the dialog is not present. " +
-                    "Will launch activity without animating."
+                    "Will launch activity without animating.",
             )
         }
 
@@ -172,11 +181,7 @@
         if (animationController == null) {
             currentDialog?.dismiss()
         }
-        activityStarter.startActivity(
-            intent,
-            true, /* dismissShade */
-            animationController,
-        )
+        activityStarter.startActivity(intent, true, /* dismissShade */ animationController)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 680df15..dcf32a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -137,7 +137,7 @@
             MediaTestUtils.emptyMediaData.copy(
                 app = PACKAGE,
                 packageName = PACKAGE,
-                token = session.sessionToken
+                token = session.sessionToken,
             )
 
         resumeData = mediaData.copy(token = null, active = false, resumption = true)
@@ -237,7 +237,7 @@
         // Assuming we're registered
         testOnMediaDataLoaded_registersPlaybackListener()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -249,7 +249,7 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(0)
@@ -261,7 +261,7 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
         )
         assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -435,7 +435,7 @@
 
         // When the playback state changes, and has different actions
         val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
         assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
@@ -448,7 +448,7 @@
             PlaybackState.CustomAction.Builder(
                     "ACTION_1",
                     "custom action 1",
-                    android.R.drawable.ic_media_ff
+                    android.R.drawable.ic_media_ff,
                 )
                 .build()
         val pausedState =
@@ -463,7 +463,7 @@
             PlaybackState.CustomAction.Builder(
                     "ACTION_2",
                     "custom action 2",
-                    android.R.drawable.ic_media_rew
+                    android.R.drawable.ic_media_rew,
                 )
                 .build()
         val pausedStateTwoActions =
@@ -472,7 +472,7 @@
                 .addCustomAction(customOne)
                 .addCustomAction(customTwo)
                 .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+        onPlaybackStateChanged(pausedStateTwoActions)
         assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
@@ -485,7 +485,7 @@
         loadMediaDataWithPlaybackState(stateWithActions)
 
         // When the playback state updates with the same actions
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
+        onPlaybackStateChanged(stateWithActions)
 
         // Then the callback is not invoked again
         verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -512,7 +512,7 @@
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customTwo)
                 .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
+        onPlaybackStateChanged(stateTwo)
 
         // Then the callback is not invoked
         verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -544,7 +544,7 @@
         // When the playback state changes to playing
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
         uiExecutor.runAllReady()
 
         // Then the callback is invoked
@@ -561,7 +561,7 @@
         // When the playback state is updated, but still not playing
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        onPlaybackStateChanged(playingState)
 
         // Then the callback is not invoked
         verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
@@ -571,7 +571,7 @@
     fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
         // When paused media is loaded
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -597,7 +597,7 @@
         val time = clock.currentTimeMillis()
         clock.setElapsedRealtime(time)
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(
+        onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -706,4 +706,9 @@
         bgExecutor.runAllReady()
         uiExecutor.runAllReady()
     }
+
+    private fun onPlaybackStateChanged(state: PlaybackState) {
+        mediaCallbackCaptor.value.onPlaybackStateChanged(state)
+        bgExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index eea02ee..2f8f45c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -887,6 +888,34 @@
     }
 
     @Test
+    public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+
+        // Adds non DDS subId
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        doReturn(false).when(info).isOpportunistic();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+        // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId()
+        verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+        assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+
+        // Adds non DDS subId again
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        doReturn(false).when(info).isOpportunistic();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+        // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap.
+        verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+        assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+    }
+
+    @Test
     public void getMobileNetworkSummary() {
         mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
         Resources res1 = mock(Resources.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 0407fc1..ac73882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -24,7 +24,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import com.android.systemui.util.DeviceConfigProxyFake
 import com.android.systemui.util.Utils
@@ -42,7 +42,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 // this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME)
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     lateinit var manager: NotificationSectionsFeatureManager
     private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index deb3fc1..a3f8452 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -15,8 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.app.Flags.lifetimeExtensionRefactor
 import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
+import android.app.Flags.lifetimeExtensionRefactor
 import android.app.Notification
 import android.app.RemoteInputHistoryItem
 import android.os.Handler
@@ -47,10 +47,10 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations.initMocks
 
 @SmallTest
@@ -78,21 +78,20 @@
     @Before
     fun setUp() {
         initMocks(this)
-        coordinator = RemoteInputCoordinator(
+        coordinator =
+            RemoteInputCoordinator(
                 dumpManager,
                 rebuilder,
                 remoteInputManager,
                 mainHandler,
-                smartReplyController
-        )
+                smartReplyController,
+            )
         `when`(pipeline.addNotificationLifetimeExtender(any())).thenAnswer {
             (it.arguments[0] as NotifLifetimeExtender).setCallback(lifetimeExtensionCallback)
         }
         `when`(pipeline.getInternalNotifUpdater(any())).thenReturn(notifUpdater)
         coordinator.attach(pipeline)
-        listener = withArgCaptor {
-            verify(remoteInputManager).setRemoteInputListener(capture())
-        }
+        listener = withArgCaptor { verify(remoteInputManager).setRemoteInputListener(capture()) }
         entry1 = NotificationEntryBuilder().setId(1).build()
         entry2 = NotificationEntryBuilder().setId(2).build()
         `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -101,13 +100,17 @@
         `when`(rebuilder.rebuildWithExistingReplies(any())).thenReturn(sbn)
     }
 
-    val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender
-    val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
-    val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+    val remoteInputActiveExtender
+        get() = coordinator.mRemoteInputActiveExtender
 
-    val collectionListeners get() = captureMany {
-        verify(pipeline, times(1)).addCollectionListener(capture())
-    }
+    val remoteInputHistoryExtender
+        get() = coordinator.mRemoteInputHistoryExtender
+
+    val smartReplyHistoryExtender
+        get() = coordinator.mSmartReplyHistoryExtender
+
+    val collectionListeners
+        get() = captureMany { verify(pipeline, times(1)).addCollectionListener(capture()) }
 
     @Test
     fun testRemoteInputActive() {
@@ -179,7 +182,8 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testRemoteInputLifetimeExtensionListenerTrigger() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
@@ -187,9 +191,7 @@
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
 
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
     }
@@ -198,16 +200,15 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testSmartReplyLifetimeExtensionListenerTrigger() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                 .build()
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
         verify(smartReplyController, times(1)).stopSending(entry)
@@ -217,25 +218,25 @@
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testRepeatedUpdateTriggersRebuild() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                 .build()
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
-        verify(rebuilder, times(1)).rebuildWithExistingReplies(entry)
+        verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
     }
 
     @Test
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testLifetimeExtensionListenerClearsRemoteInputs() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
-        val entry = NotificationEntryBuilder()
+        val entry =
+            NotificationEntryBuilder()
                 .setId(3)
                 .setTag("entry")
                 .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
@@ -245,9 +246,7 @@
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
 
-        collectionListeners.forEach {
-            it.onEntryUpdated(entry, true)
-        }
+        collectionListeners.forEach { it.onEntryUpdated(entry, true) }
 
         assertThat(entry.remoteInputs).isNull()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 83ad18b..46f3a6b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ui.isAnimating
@@ -254,6 +255,39 @@
             assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text)
         }
 
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun manageButtonOnClick_whenHistoryDisabled() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.manageOrHistoryButtonClick)
+            runCurrent()
+
+            // WHEN notification history is disabled
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)
+
+            // THEN onClick leads to settings page
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS)
+            assertThat(onClick?.backStack).isEmpty()
+        }
+
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+    @Test
+    fun historyButtonOnClick_whenHistoryEnabled() =
+        testScope.runTest {
+            val onClick by collectLastValue(underTest.manageOrHistoryButtonClick)
+            runCurrent()
+
+            // WHEN notification history is enabled
+            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)
+
+            // THEN onClick leads to history page
+            assertThat(onClick?.targetIntent?.action)
+                .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY)
+            assertThat(onClick?.backStack?.map { it.action })
+                .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
+        }
+
     @Test
     fun manageButtonVisible_whenMessageVisible() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index e2b283b..38bc758 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -34,7 +33,6 @@
             transitionInteractor = keyguardTransitionInteractor,
             dismissInteractor = keyguardDismissInteractor,
             applicationScope = testScope.backgroundScope,
-            sceneInteractor = { sceneInteractor },
             deviceUnlockedInteractor = { deviceUnlockedInteractor },
             powerInteractor = powerInteractor,
             alternateBouncerInteractor = alternateBouncerInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
index b19e221..3d2bd6c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
@@ -45,3 +45,17 @@
             UserHandle.USER_CURRENT,
         )
     }
+
+var Kosmos.lockScreenNotificationMinimalismSetting: Boolean
+    get() =
+        fakeSettings.getIntForUser(
+            Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            UserHandle.USER_CURRENT,
+        ) == 1
+    set(value) {
+        fakeSettings.putIntForUser(
+            Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+            if (value) 1 else 0,
+            UserHandle.USER_CURRENT,
+        )
+    }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c8f8c2a..082459b 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -21,11 +21,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
+import android.app.appfunctions.AppFunctionService;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.ExecuteAppFunctionResponse;
 import android.app.appfunctions.IAppFunctionManager;
 import android.app.appfunctions.IAppFunctionService;
+import android.app.appfunctions.ICancellationCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
 import android.app.appsearch.AppSearchManager;
@@ -37,11 +39,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
 import com.android.server.SystemService.TargetUser;
 import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
 import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
@@ -99,7 +105,7 @@
     }
 
     @Override
-    public void executeAppFunction(
+    public ICancellationSignal executeAppFunction(
             @NonNull ExecuteAppFunctionAidlRequest requestInternal,
             @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
         Objects.requireNonNull(requestInternal);
@@ -120,11 +126,14 @@
                             ExecuteAppFunctionResponse.RESULT_DENIED,
                             exception.getMessage(),
                             /* extras= */ null));
-            return;
+            return null;
         }
 
         int callingUid = Binder.getCallingUid();
-        int callingPid = Binder.getCallingUid();
+        int callingPid = Binder.getCallingPid();
+
+        ICancellationSignal localCancelTransport = CancellationSignal.createTransport();
+
         THREAD_POOL_EXECUTOR.execute(
                 () -> {
                     try {
@@ -132,12 +141,14 @@
                                 requestInternal,
                                 callingUid,
                                 callingPid,
+                                localCancelTransport,
                                 safeExecuteAppFunctionCallback);
                     } catch (Exception e) {
                         safeExecuteAppFunctionCallback.onResult(
                                 mapExceptionToExecuteAppFunctionResponse(e));
                     }
                 });
+        return localCancelTransport;
     }
 
     @WorkerThread
@@ -145,6 +156,7 @@
             ExecuteAppFunctionAidlRequest requestInternal,
             int callingUid,
             int callingPid,
+            ICancellationSignal localCancelTransport,
             SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
         UserHandle targetUser = requestInternal.getUserHandle();
         // TODO(b/354956319): Add and honor the new enterprise policies.
@@ -203,6 +215,7 @@
                                             requestInternal,
                                             serviceIntent,
                                             targetUser,
+                                            localCancelTransport,
                                             safeExecuteAppFunctionCallback,
                                             /* bindFlags= */ Context.BIND_AUTO_CREATE
                                                     | Context.BIND_FOREGROUND_SERVICE);
@@ -219,8 +232,19 @@
             @NonNull ExecuteAppFunctionAidlRequest requestInternal,
             @NonNull Intent serviceIntent,
             @NonNull UserHandle targetUser,
+            @NonNull ICancellationSignal cancellationSignalTransport,
             @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback,
             int bindFlags) {
+        CancellationSignal cancellationSignal =
+                CancellationSignal.fromTransport(cancellationSignalTransport);
+        ICancellationCallback cancellationCallback =
+                new ICancellationCallback.Stub() {
+                    @Override
+                    public void sendCancellationTransport(
+                            @NonNull ICancellationSignal cancellationTransport) {
+                        cancellationSignal.setRemote(cancellationTransport);
+                    }
+                };
         boolean bindServiceResult =
                 mRemoteServiceCaller.runServiceCall(
                         serviceIntent,
@@ -236,6 +260,7 @@
                                 try {
                                     service.executeAppFunction(
                                             requestInternal.getClientRequest(),
+                                            cancellationCallback,
                                             new IExecuteAppFunctionCallback.Stub() {
                                                 @Override
                                                 public void onResult(
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 0713999..60b826b 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -41,6 +41,7 @@
     public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3;
     public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4;
     public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5;
+    public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6;
 
     /** @hide */
     @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
@@ -50,6 +51,7 @@
             CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
             CPU_WAKEUP_SUBSYSTEM_SENSOR,
             CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+            CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CpuWakeupSubsystem {
@@ -99,6 +101,14 @@
     public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid);
 
     /**
+     * Informs battery stats of a sysproxy packet that woke up the CPU
+     *
+     * @param uid The uid that received the packet.
+     * @param elapsedMillis The time of the packet's arrival in elapsed timebase.
+     */
+    public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis);
+
+    /**
      * Informs battery stats of binder stats for the given work source UID.
      */
     public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount,
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index d86bae1..e64a480 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -219,7 +219,7 @@
     public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10;
 
     /** Extended timeout for the system server watchdog. */
-    private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000;
+    private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000;
 
     /** Extended timeout for the system server watchdog for vold#partition operation. */
     private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000;
@@ -3251,7 +3251,7 @@
         if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
             throw new SecurityException("no permission to commit checkpoint changes");
         }
-
+        extendWatchdogTimeout("vold#commitChanges might be slow");
         mVold.commitChanges();
     }
 
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 15277ce..ef82c74 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -664,6 +664,8 @@
             } else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
                 return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
             }
+            // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups.
+            // See noteCpuWakingSysproxyPacket method.
             return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
         }
 
@@ -686,6 +688,15 @@
         }
 
         @Override
+        public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) {
+            if (uid < 0) {
+                Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid);
+                return;
+            }
+            noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid);
+        }
+
+        @Override
         public void noteBinderCallStats(int workSourceUid, long incrementatCallCount,
                 Collection<BinderCallsStats.CallStat> callStats) {
             synchronized (BatteryStatsService.this.mLock) {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a6389f7..e0cf96f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1031,6 +1031,9 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
             String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern();
             int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
 
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 88907e3..1f9eb08 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3377,10 +3377,18 @@
     private void dumpInternal(PrintWriter pw) {
         pw.println("DISPLAY MANAGER (dumpsys display)");
         BrightnessTracker brightnessTrackerLocal;
+        SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>();
+        int displayPowerControllerCount;
 
         synchronized (mSyncRoot) {
             brightnessTrackerLocal = mBrightnessTracker;
 
+            displayPowerControllerCount = mDisplayPowerControllers.size();
+            for (int i = 0; i < displayPowerControllerCount; i++) {
+                displayPowerControllersLocal.put(
+                        mDisplayPowerControllers.keyAt(i), mDisplayPowerControllers.valueAt(i));
+            }
+
             pw.println("  mSafeMode=" + mSafeMode);
             pw.println("  mPendingTraversal=" + mPendingTraversal);
             pw.println("  mViewports=" + mViewports);
@@ -3451,13 +3459,6 @@
                         + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested);
             }
 
-            final int displayPowerControllerCount = mDisplayPowerControllers.size();
-            pw.println();
-            pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
-            for (int i = 0; i < displayPowerControllerCount; i++) {
-                mDisplayPowerControllers.valueAt(i).dump(pw);
-            }
-
             pw.println();
             mPersistentDataStore.dump(pw);
 
@@ -3470,6 +3471,12 @@
                 mDisplayWindowPolicyControllers.valueAt(i).second.dump("  ", pw);
             }
         }
+        pw.println();
+        pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
+        for (int i = 0; i < displayPowerControllerCount; i++) {
+            displayPowerControllersLocal.valueAt(i).dump(pw);
+        }
+
         if (brightnessTrackerLocal != null) {
             pw.println();
             brightnessTrackerLocal.dump(pw);
diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig
index 2f817db..3453668 100644
--- a/services/core/java/com/android/server/flags/pinner.aconfig
+++ b/services/core/java/com/android/server/flags/pinner.aconfig
@@ -9,8 +9,11 @@
 }
 
 flag {
-    name: "skip_home_art_pins"
-    namespace: "system_performance"
-    description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory."
-    bug: "340935152"
-}
\ No newline at end of file
+  name: "pin_global_quota"
+  namespace: "system_performance"
+  description: "This flag controls whether pinner will use a global quota or not"
+  bug: "340935152"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 48d24f2..47f579d 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -195,10 +195,9 @@
                     == PackageManager.PERMISSION_GRANTED) {
                 return true;
             }
-            boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA,
-                    mProjectionGrant.uid,
-                    mProjectionGrant.packageName);
-            if (operationActive) {
+            if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA,
+                    mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null,
+                    "recording lockscreen")) {
                 // Some tools use media projection by granting the OP_PROJECT_MEDIA app
                 // op via a shell command. Those tools can be granted keyguard capture
                 return true;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c3a714b..79633f1 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2579,6 +2579,7 @@
                 mNotificationChannelLogger,
                 mAppOps,
                 mUserProfiles,
+                mUgmInternal,
                 mShowReviewPermissionsNotification,
                 Clock.systemUTC());
         mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
@@ -6672,13 +6673,7 @@
             final Uri originalSoundUri =
                     (originalChannel != null) ? originalChannel.getSound() : null;
             if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) {
-                Binder.withCleanCallingIdentity(() -> {
-                    mUgmInternal.checkGrantUriPermission(sourceUid, null,
-                            ContentProvider.getUriWithoutUserId(soundUri),
-                            Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                            ContentProvider.getUserIdFromUri(soundUri,
-                            UserHandle.getUserId(sourceUid)));
-                });
+                PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid);
             }
         }
 
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index b9f0968..3ba9384 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -1493,14 +1493,23 @@
 
             final Notification notification = getNotification();
             notification.visitUris((uri) -> {
-                visitGrantableUri(uri, false, false);
+                if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+                    visitGrantableUri(uri, false, false);
+                } else {
+                    oldVisitGrantableUri(uri, false, false);
+                }
             });
 
             if (notification.getChannelId() != null) {
                 NotificationChannel channel = getChannel();
                 if (channel != null) {
-                    visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
-                            & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+                        visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    } else {
+                        oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                    }
                 }
             }
         } finally {
@@ -1516,7 +1525,7 @@
      * {@link #mGrantableUris}. Otherwise, this will either log or throw
      * {@link SecurityException} depending on target SDK of enqueuing app.
      */
-    private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
+    private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
         if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
 
         if (mGrantableUris != null && mGrantableUris.contains(uri)) {
@@ -1555,6 +1564,45 @@
         }
     }
 
+    /**
+     * Note the presence of a {@link Uri} that should have permission granted to
+     * whoever will be rendering it.
+     * <p>
+     * If the enqueuing app has the ability to grant access, it will be added to
+     * {@link #mGrantableUris}. Otherwise, this will either log or throw
+     * {@link SecurityException} depending on target SDK of enqueuing app.
+     */
+    private void visitGrantableUri(Uri uri, boolean userOverriddenUri,
+            boolean isSound) {
+        if (mGrantableUris != null && mGrantableUris.contains(uri)) {
+            return; // already verified this URI
+        }
+
+        final int sourceUid = getSbn().getUid();
+        try {
+            PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid);
+
+            if (mGrantableUris == null) {
+                mGrantableUris = new ArraySet<>();
+            }
+            mGrantableUris.add(uri);
+        } catch (SecurityException e) {
+            if (!userOverriddenUri) {
+                if (isSound) {
+                    mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
+                    Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage());
+                } else {
+                    if (mTargetSdkVersion >= Build.VERSION_CODES.P) {
+                        throw e;
+                    } else {
+                        Log.w(TAG,
+                                "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
     public LogMaker getLogMaker(long now) {
         LogMaker lm = getSbn().getLogMaker()
                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance)
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index b6f4889..1464d48 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -25,19 +25,25 @@
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.companion.virtual.VirtualDeviceManager;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.permission.IPermissionManager;
 import android.util.ArrayMap;
 import android.util.Pair;
 import android.util.Slog;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -58,7 +64,7 @@
     private final IPermissionManager mPermManager;
 
     public PermissionHelper(Context context, IPackageManager packageManager,
-            IPermissionManager permManager) {
+                IPermissionManager permManager) {
         mContext = context;
         mPackageManager = packageManager;
         mPermManager = permManager;
@@ -298,6 +304,19 @@
         return false;
     }
 
+    static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri,
+            int sourceUid) {
+        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
+
+        Binder.withCleanCallingIdentity(() -> {
+            // This will throw a SecurityException if the caller can't grant.
+            ugmInternal.checkGrantUriPermission(sourceUid, null,
+                    ContentProvider.getUriWithoutUserId(uri),
+                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
+        });
+    }
+
     public static class PackagePermission {
         public final String packageName;
         public final @UserIdInt int userId;
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 85c3957..9e70f81 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -94,6 +94,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -219,6 +220,7 @@
     private final NotificationChannelLogger mNotificationChannelLogger;
     private final AppOpsManager mAppOps;
     private final ManagedServices.UserProfiles mUserProfiles;
+    private final UriGrantsManagerInternal mUgmInternal;
 
     private SparseBooleanArray mBadgingEnabled;
     private SparseBooleanArray mBubblesEnabled;
@@ -239,6 +241,7 @@
             ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
             NotificationChannelLogger notificationChannelLogger,
             AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+            UriGrantsManagerInternal ugmInternal,
             boolean showReviewPermissionsNotification, Clock clock) {
         mContext = context;
         mZenModeHelper = zenHelper;
@@ -249,6 +252,7 @@
         mNotificationChannelLogger = notificationChannelLogger;
         mAppOps = appOpsManager;
         mUserProfiles = userProfiles;
+        mUgmInternal = ugmInternal;
         mShowReviewPermissionsNotification = showReviewPermissionsNotification;
         mIsMediaNotificationFilteringEnabled = context.getResources()
                 .getBoolean(R.bool.config_quickSettingsShowMediaPlayer);
@@ -1169,6 +1173,13 @@
                 }
                 clearLockedFieldsLocked(channel);
 
+                // Verify that the app has permission to read the sound Uri
+                // Only check for new channels, as regular apps can only set sound
+                // before creating. See: {@link NotificationChannel#setSound}
+                if (Flags.notificationVerifyChannelSoundUri()) {
+                    PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
+                }
+
                 channel.setImportanceLockedByCriticalDeviceFunction(
                         r.defaultAppLockedImportance || r.fixedImportance);
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index be3adc1..0b34177 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -144,6 +144,13 @@
 }
 
 flag {
+  name: "notification_minimalism"
+  namespace: "systemui"
+  description: "Minimize the notifications to show on the lockscreen."
+  bug: "330387368"
+}
+
+flag {
   name: "notification_force_group_singletons"
   namespace: "systemui"
   description: "This flag enables forced auto-grouping singleton groups"
@@ -163,3 +170,13 @@
   description: "This flag enables sound uri with vibration source"
   bug: "358524009"
 }
+
+flag {
+  name: "notification_verify_channel_sound_uri"
+  namespace: "systemui"
+  description: "Verify Uri permission for sound when creating a notification channel"
+  bug: "337775777"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java
new file mode 100644
index 0000000..5f96411
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSource.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+/* package */ abstract class PinRangeSource {
+    /**
+     * Retrieve a range to pin.
+     *
+     * @param outPinRange Receives the pin region
+     * @return True if we filled in outPinRange or false if we're out of pin entries
+     */
+    abstract boolean read(PinnerService.PinRange outPinRange);
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
new file mode 100644
index 0000000..d6fc487
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+/* package */ class PinRangeSourceStatic extends PinRangeSource {
+    private final int mPinStart;
+    private final int mPinLength;
+    private boolean mDone = false;
+
+    PinRangeSourceStatic(int pinStart, int pinLength) {
+        mPinStart = pinStart;
+        mPinLength = pinLength;
+    }
+
+    @Override
+    boolean read(PinnerService.PinRange outPinRange) {
+        outPinRange.start = mPinStart;
+        outPinRange.length = mPinLength;
+        boolean done = mDone;
+        mDone = true;
+        return !done;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
new file mode 100644
index 0000000..79900b9
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/* package */ final class PinRangeSourceStream extends PinRangeSource {
+    private final DataInputStream mStream;
+    private boolean mDone = false;
+
+    PinRangeSourceStream(InputStream stream) {
+        mStream = new DataInputStream(stream);
+    }
+
+    @Override
+    boolean read(PinnerService.PinRange outPinRange) {
+        if (!mDone) {
+            try {
+                outPinRange.start = mStream.readInt();
+                outPinRange.length = mStream.readInt();
+            } catch (IOException ex) {
+                mDone = true;
+            }
+        }
+        return !mDone;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java
new file mode 100644
index 0000000..a8de344
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnedFile.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+@VisibleForTesting
+public final class PinnedFile implements AutoCloseable {
+    private long mAddress;
+    final long mapSize;
+    final String fileName;
+    public final long bytesPinned;
+
+    // Whether this file was pinned using a pinlist
+    boolean used_pinlist;
+
+    // User defined group name for pinner accounting
+    String groupName = "";
+    ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
+
+    public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) {
+        mAddress = address;
+        this.mapSize = mapSize;
+        this.fileName = fileName;
+        this.bytesPinned = bytesPinned;
+    }
+
+    @Override
+    public void close() {
+        if (mAddress >= 0) {
+            PinnerUtils.safeMunmap(mAddress, mapSize);
+            mAddress = -1;
+        }
+        for (PinnedFile dep : pinnedDeps) {
+            if (dep != null) {
+                dep.close();
+            }
+        }
+    }
+
+    @Override
+    public void finalize() {
+        close();
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java
similarity index 76%
rename from services/core/java/com/android/server/PinnerService.java
rename to services/core/java/com/android/server/pinner/PinnerService.java
index ef03888..d7ac520 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/pinner/PinnerService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.server.pinner;
 
 import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
 import static android.app.ActivityManager.UID_OBSERVER_GONE;
 import static android.os.Process.SYSTEM_UID;
 
+import static com.android.server.flags.Flags.pinGlobalQuota;
 import static com.android.server.flags.Flags.pinWebview;
-import static com.android.server.flags.Flags.skipHomeArtPins;
 
 import android.annotation.EnforcePermission;
 import android.annotation.IntDef;
@@ -49,6 +49,7 @@
 import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -72,13 +73,13 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import dalvik.system.DexFile;
 import dalvik.system.VMRuntime;
 
-import java.io.Closeable;
-import java.io.DataInputStream;
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -110,8 +111,7 @@
     private static final String PIN_META_FILENAME = "pinlist.meta";
     private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
     private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY
-            | PackageManager.MATCH_DIRECT_BOOT_AWARE
-            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+            | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 
     private static final int KEY_CAMERA = 0;
     private static final int KEY_HOME = 1;
@@ -126,6 +126,8 @@
 
     public static final String ANON_REGION_STAT_NAME = "[anon]";
 
+    private static final String SYSTEM_GROUP_NAME = "system";
+
     @IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface AppKey {}
@@ -139,7 +141,8 @@
     private final UserManager mUserManager;
 
     /** The list of the statically pinned files. */
-    @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
+    @GuardedBy("this")
+    private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
 
     /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */
     @GuardedBy("this")
@@ -159,8 +162,8 @@
     /**
      * A set of {@link AppKey} that are configured to be pinned.
      */
-    @GuardedBy("this")
-    private ArraySet<Integer> mPinKeys;
+    @GuardedBy("this") private
+    ArraySet<Integer> mPinKeys;
 
     // Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want
     // them to be responsive to dynamic flag changes for experimentation.
@@ -180,14 +183,23 @@
     private final boolean mConfiguredToPinAssistant;
     private final int mConfiguredWebviewPinBytes;
 
+    // This is the percentage of total device memory that will be used to set the global quota.
+    private final int mConfiguredMaxPinnedMemoryPercentage;
+
+    // This is the global pinner quota that can be pinned.
+    private long mConfiguredMaxPinnedMemory;
+
+    // This is the currently pinned memory.
+    private long mCurrentPinnedMemory = 0;
+
     private BinderService mBinderService;
     private PinnerHandler mPinnerHandler = null;
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-          // If an app has updated, update pinned files accordingly.
-          if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
+            // If an app has updated, update pinned files accordingly.
+            if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
                 Uri packageUri = intent.getData();
                 String packageName = packageUri.getSchemeSpecificPart();
                 ArraySet<String> updatedPackages = new ArraySet<>();
@@ -210,7 +222,7 @@
 
     /** Utility class for testing. */
     @VisibleForTesting
-    static class Injector {
+    public static class Injector {
         protected DeviceConfigInterface getDeviceConfigInterface() {
             return DeviceConfigInterface.REAL;
         }
@@ -219,9 +231,9 @@
             service.publishBinderService("pinner", binderService);
         }
 
-        protected PinnedFile pinFileInternal(String fileToPin,
-                int maxBytesToPin, boolean attemptPinIntrospection) {
-            return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
+        protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+                long maxBytesToPin, boolean attemptPinIntrospection) {
+            return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
         }
     }
 
@@ -230,7 +242,7 @@
     }
 
     @VisibleForTesting
-    PinnerService(Context context, Injector injector) {
+    public PinnerService(Context context, Injector injector) {
         super(context);
 
         mContext = context;
@@ -244,6 +256,9 @@
                 com.android.internal.R.bool.config_pinnerAssistantApp);
         mConfiguredWebviewPinBytes = context.getResources().getInteger(
                 com.android.internal.R.integer.config_pinnerWebviewPinBytes);
+        mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage);
+
         mPinKeys = createPinKeys();
         mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
 
@@ -261,10 +276,8 @@
         registerUidListener();
         registerUserSetupCompleteListener();
 
-        mDeviceConfigInterface.addOnPropertiesChangedListener(
-                DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
-                new HandlerExecutor(mPinnerHandler),
-                mDeviceConfigAnonSizeListener);
+        mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
+                new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener);
     }
 
     @Override
@@ -272,6 +285,10 @@
         if (DEBUG) {
             Slog.i(TAG, "Starting PinnerService");
         }
+        mConfiguredMaxPinnedMemory =
+                (Process.getTotalMemory()
+                        * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100))
+                / 100;
         mBinderService = new BinderService();
         mInjector.publishBinderService(this, mBinderService);
         publishLocalService(PinnerService.class, this);
@@ -348,7 +365,7 @@
         protected PinnedFileStats(int uid, PinnedFile file) {
             this.uid = uid;
             this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1);
-            this.sizeKb = file.bytesPinned / 1024;
+            this.sizeKb = (int) file.bytesPinned / 1024;
         }
     }
 
@@ -358,20 +375,11 @@
     private void handlePinOnStart() {
         // Files to pin come from the overlay and can be specified per-device config
         String[] filesToPin = mContext.getResources().getStringArray(
-            com.android.internal.R.array.config_defaultPinnerServiceFiles);
+                com.android.internal.R.array.config_defaultPinnerServiceFiles);
         // Continue trying to pin each file even if we fail to pin some of them
         for (String fileToPin : filesToPin) {
-            PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE,
-                    /*attemptPinIntrospection=*/false);
-            if (pf == null) {
-                Slog.e(TAG, "Failed to pin file = " + fileToPin);
-                continue;
-            }
-            synchronized (this) {
-                mPinnedFiles.put(pf.fileName, pf);
-            }
-            pf.groupName = "system";
-            pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null);
+            pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME,
+                    true);
         }
 
         refreshPinAnonConfig();
@@ -383,10 +391,9 @@
      * regular home app.
      */
     private void registerUserSetupCompleteListener() {
-        Uri userSetupCompleteUri = Settings.Secure.getUriFor(
-                Settings.Secure.USER_SETUP_COMPLETE);
-        mContext.getContentResolver().registerContentObserver(userSetupCompleteUri,
-                false, new ContentObserver(null) {
+        Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+        mContext.getContentResolver().registerContentObserver(
+                userSetupCompleteUri, false, new ContentObserver(null) {
                     @Override
                     public void onChange(boolean selfChange, Uri uri) {
                         if (userSetupCompleteUri.equals(uri)) {
@@ -409,7 +416,7 @@
                 }
 
                 @Override
-                public void onUidActive(int uid)  {
+                public void onUidActive(int uid) {
                     mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
                             PinnerService::handleUidActive, PinnerService.this, uid));
                 }
@@ -423,7 +430,6 @@
         updateActiveState(uid, false /* active */);
         int key;
         synchronized (this) {
-
             // In case we have a pending repin, repin now. See mPendingRepin for more information.
             key = mPendingRepin.getOrDefault(uid, -1);
             if (key == -1) {
@@ -491,8 +497,8 @@
 
     private ApplicationInfo getCameraInfo(int userHandle) {
         Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
-        ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle,
-            false /* defaultToSystemApp */);
+        ApplicationInfo info = getApplicationInfoForIntent(
+                cameraIntent, userHandle, false /* defaultToSystemApp */);
 
         // If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent.
         // We don't use _SECURE first because it will never get set on a device
@@ -501,16 +507,16 @@
         // preference using this intent.
         if (info == null) {
             cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE);
-            info = getApplicationInfoForIntent(cameraIntent, userHandle,
-                false /* defaultToSystemApp */);
+            info = getApplicationInfoForIntent(
+                    cameraIntent, userHandle, false /* defaultToSystemApp */);
         }
 
         // If the _SECURE intent doesn't resolve, try the original intent but request
         // the system app for camera if there was more than one result.
         if (info == null) {
             cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
-            info = getApplicationInfoForIntent(cameraIntent, userHandle,
-                true /* defaultToSystemApp */);
+            info = getApplicationInfoForIntent(
+                    cameraIntent, userHandle, true /* defaultToSystemApp */);
         }
         return info;
     }
@@ -525,14 +531,14 @@
         return getApplicationInfoForIntent(intent, userHandle, true);
     }
 
-    private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle,
-            boolean defaultToSystemApp) {
+    private ApplicationInfo getApplicationInfoForIntent(
+            Intent intent, int userHandle, boolean defaultToSystemApp) {
         if (intent == null) {
             return null;
         }
 
-        ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent,
-                MATCH_FLAGS, userHandle);
+        ResolveInfo resolveInfo =
+                mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle);
 
         // If this intent can resolve to only one app, choose that one.
         // Otherwise, if we've requested to default to the system app, return it;
@@ -547,12 +553,11 @@
         }
 
         if (defaultToSystemApp) {
-            List<ResolveInfo> infoList = mContext.getPackageManager()
-                .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle);
+            List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser(
+                    intent, MATCH_FLAGS, userHandle);
             ApplicationInfo systemAppInfo = null;
             for (ResolveInfo info : infoList) {
-                if ((info.activityInfo.applicationInfo.flags
-                      & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                     if (systemAppInfo == null) {
                         systemAppInfo = info.activityInfo.applicationInfo;
                     } else {
@@ -568,13 +573,13 @@
     }
 
     private void sendPinAppsMessage(int userHandle) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this,
-                userHandle));
+        mPinnerHandler.sendMessage(
+                PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle));
     }
 
     private void sendPinAppsWithUpdatedKeysMessage(int userHandle) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys,
-                this, userHandle));
+        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
+                PinnerService::pinAppsWithUpdatedKeys, this, userHandle));
     }
     private void sendUnpinAppsMessage() {
         mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this));
@@ -586,8 +591,7 @@
         // phenotype property is not set.
         boolean shouldPinCamera = mConfiguredToPinCamera
                 && mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
-                        "pin_camera",
-                        SystemProperties.getBoolean("pinner.pin_camera", true));
+                        "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true));
         if (shouldPinCamera) {
             pinKeys.add(KEY_CAMERA);
         } else if (DEBUG) {
@@ -626,8 +630,9 @@
             synchronized (this) {
                 // This code path demands preceding unpinApps() call.
                 if (!mPinnedApps.isEmpty()) {
-                    Slog.e(TAG, "Attempted to update a list of apps, "
-                            + "but apps were already pinned. Skipping.");
+                    Slog.e(TAG,
+                            "Attempted to update a list of apps, "
+                                    + "but apps were already pinned. Skipping.");
                     return;
                 }
 
@@ -646,8 +651,8 @@
      * @see #pinApp(int, int, boolean)
      */
     private void sendPinAppMessage(int key, int userHandle, boolean force) {
-        mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this,
-                key, userHandle, force));
+        mPinnerHandler.sendMessage(
+                PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force));
     }
 
     /**
@@ -667,10 +672,10 @@
             }
             return;
         }
-        unpinApp(key);
         ApplicationInfo info = getInfoForKey(key, userHandle);
+        unpinApp(key);
         if (info != null) {
-            pinApp(key, info);
+            pinAppInternal(key, info);
         }
     }
 
@@ -682,9 +687,7 @@
     private int getUidForKey(@AppKey int key) {
         synchronized (this) {
             PinnedApp existing = mPinnedApps.get(key);
-            return existing != null && existing.active
-                    ? existing.uid
-                    : -1;
+            return existing != null && existing.active ? existing.uid : -1;
         }
     }
 
@@ -727,11 +730,8 @@
      * Handle any changes in the anon region pinner config.
      */
     private void refreshPinAnonConfig() {
-        long newPinAnonSize =
-                mDeviceConfigInterface.getLong(
-                        DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
-                        DEVICE_CONFIG_KEY_ANON_SIZE,
-                        DEFAULT_ANON_SIZE);
+        long newPinAnonSize = mDeviceConfigInterface.getLong(
+                DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE);
         newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE));
         if (newPinAnonSize != mPinAnonSize) {
             mPinAnonSize = newPinAnonSize;
@@ -765,10 +765,9 @@
         try {
             // Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status).
             // The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo.
-            address = Os.mmap(0, alignedPinSize,
-                    OsConstants.PROT_READ | OsConstants.PROT_WRITE,
-                    OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS,
-                    new FileDescriptor(), /*offset=*/0);
+            address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE,
+                    OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(),
+                    /*offset=*/0);
 
             Unsafe tempUnsafe = null;
             Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class;
@@ -794,14 +793,14 @@
             return;
         } finally {
             if (address >= 0) {
-                safeMunmap(address, alignedPinSize);
+                PinnerUtils.safeMunmap(address, alignedPinSize);
             }
         }
     }
 
     private void unpinAnonRegion() {
         if (mPinAnonAddress != 0) {
-            safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
+            PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
         }
         mPinAnonAddress = 0;
         mCurrentlyPinnedAnonSize = 0;
@@ -824,12 +823,20 @@
     }
 
     /**
+     * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer
+     * pin any file.
+     */
+    private long getAvailableGlobalQuota() {
+        return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory;
+    }
+
+    /**
      * Pins an application.
      *
      * @param key The key of the app to pin.
      * @param appInfo The corresponding app info.
      */
-    private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) {
+    private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) {
         if (appInfo == null) {
             return;
         }
@@ -839,7 +846,6 @@
             mPinnedApps.put(key, pinnedApp);
         }
 
-
         // pin APK
         final int pinSizeLimit = getSizeLimitForKey(key);
         List<String> apks = new ArrayList<>();
@@ -851,36 +857,31 @@
             }
         }
 
-        int apkPinSizeLimit = pinSizeLimit;
+        long apkPinSizeLimit = pinSizeLimit;
 
-        boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins();
-
-        for (String apk: apks) {
+        for (String apk : apks) {
             if (apkPinSizeLimit <= 0) {
                 Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk);
                 // Continue instead of break to print all skipped APK names.
                 continue;
             }
 
-            PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true);
+            String pinGroup = getNameForKey(key);
+            boolean shouldPinDeps = apk.equals(appInfo.sourceDir);
+            PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps);
             if (pf == null) {
                 Slog.e(TAG, "Failed to pin " + apk);
                 continue;
             }
-            pf.groupName = getNameForKey(key);
 
             if (DEBUG) {
                 Slog.i(TAG, "Pinned " + pf.fileName);
             }
             synchronized (this) {
                 pinnedApp.mFiles.add(pf);
-                mPinnedFiles.put(pf.fileName, pf);
             }
 
             apkPinSizeLimit -= pf.bytesPinned;
-            if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) {
-                pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo);
-            }
         }
     }
 
@@ -892,19 +893,23 @@
      * that related to the file but not within itself.
      *
      * @param fileToPin File to pin
-     * @param maxBytesToPin maximum quota allowed for pinning
-     * @return total bytes that were pinned.
+     * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}.
+     * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned.
+     *                         Note: {@code bytesRequestedToPin} limit will not apply to optimized
+     *                         dependencies pinned, only global quotas will apply instead.
+     * @return pinned file
      */
-    public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo,
-            @Nullable String groupName) {
+    public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin,
+            @Nullable ApplicationInfo appInfo, @Nullable String groupName,
+            boolean pinOptimizedDeps) {
         PinnedFile existingPin;
-        synchronized(this) {
+        synchronized (this) {
             existingPin = mPinnedFiles.get(fileToPin);
         }
         if (existingPin != null) {
-            if (existingPin.bytesPinned == maxBytesToPin) {
+            if (existingPin.bytesPinned == bytesRequestedToPin) {
                 // Duplicate pin requesting same amount of bytes, lets just bail out.
-                return 0;
+                return null;
             } else {
                 // User decided to pin a different amount of bytes than currently pinned
                 // so this is a valid pin request. Unpin the previous version before repining.
@@ -915,26 +920,38 @@
             }
         }
 
+        long remainingQuota = getAvailableGlobalQuota();
+
+        if (pinGlobalQuota()) {
+            if (remainingQuota <= 0) {
+                Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin);
+                return null;
+            }
+            bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota);
+        }
+
         boolean isApk = fileToPin.endsWith(".apk");
-        int bytesPinned = 0;
-        PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin,
+
+        PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin,
                 /*attemptPinIntrospection=*/isApk);
         if (pf == null) {
             Slog.e(TAG, "Failed to pin file = " + fileToPin);
-            return 0;
+            return null;
         }
         pf.groupName = groupName != null ? groupName : "";
 
-        bytesPinned += pf.bytesPinned;
-        maxBytesToPin -= bytesPinned;
+        mCurrentPinnedMemory += pf.bytesPinned;
 
         synchronized (this) {
             mPinnedFiles.put(pf.fileName, pf);
         }
-        if (maxBytesToPin > 0) {
-            pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo);
+
+        if (pinOptimizedDeps) {
+            mCurrentPinnedMemory +=
+                    pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo);
         }
-        return bytesPinned;
+
+        return pf;
     }
 
     /**
@@ -945,13 +962,13 @@
      *                to null it will use the default supported ABI by the device.
      * @return total bytes pinned.
      */
-    private int pinOptimizedDexDependencies(
-            PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) {
+    private long pinOptimizedDexDependencies(
+            PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) {
         if (pinnedFile == null) {
             return 0;
         }
 
-        int bytesPinned = 0;
+        long bytesPinned = 0;
         if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) {
             String abi = null;
             if (appInfo != null) {
@@ -974,7 +991,7 @@
                 // Unpin if it was already pinned prior to re-pinning.
                 unpinFile(file);
 
-                PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin,
+                PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin,
                         /*attemptPinIntrospection=*/false);
                 if (df == null) {
                     Slog.i(TAG, "Failed to pin ART file = " + file);
@@ -992,7 +1009,8 @@
         return bytesPinned;
     }
 
-    /** mlock length bytes of fileToPin in memory
+    /**
+     * mlock length bytes of fileToPin in memory
      *
      * If attemptPinIntrospection is true, then treat the file to pin as a zip file and
      * look for a "pinlist.meta" file in the archive root directory. The structure of this
@@ -1029,8 +1047,8 @@
      *   zip in order to extract the
      * @return Pinned memory resource owner thing or null on error
      */
-    private static PinnedFile pinFileInternal(
-            String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) {
+    private PinnedFile pinFileInternal(
+            String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) {
         if (DEBUG) {
             Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection);
         }
@@ -1054,8 +1072,8 @@
             }
             return pinnedFile;
         } finally {
-            safeClose(pinRangeStream);
-            safeClose(fileAsZip);  // Also closes any streams we've opened
+            PinnerUtils.safeClose(pinRangeStream);
+            PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened
         }
     }
 
@@ -1068,11 +1086,8 @@
         try {
             zip = new ZipFile(fileName);
         } catch (IOException ex) {
-            Slog.w(TAG,
-                   String.format(
-                       "could not open \"%s\" as zip: pinning as blob",
-                                 fileName),
-                   ex);
+            Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName),
+                    ex);
         }
         return zip;
     }
@@ -1112,9 +1127,9 @@
                 pinMetaStream = zipFile.getInputStream(pinMetaEntry);
             } catch (IOException ex) {
                 Slog.w(TAG,
-                       String.format("error reading pin metadata \"%s\": pinning as blob",
-                                     fileName),
-                       ex);
+                        String.format(
+                                "error reading pin metadata \"%s\": pinning as blob", fileName),
+                        ex);
             }
         } else {
             Slog.w(TAG,
@@ -1124,57 +1139,6 @@
         return pinMetaStream;
     }
 
-    private static abstract class PinRangeSource {
-        /** Retrive a range to pin.
-         *
-         * @param outPinRange Receives the pin region
-         * @return True if we filled in outPinRange or false if we're out of pin entries
-         */
-        abstract boolean read(PinRange outPinRange);
-    }
-
-    private static final class PinRangeSourceStatic extends PinRangeSource {
-        private final int mPinStart;
-        private final int mPinLength;
-        private boolean mDone = false;
-
-        PinRangeSourceStatic(int pinStart, int pinLength) {
-            mPinStart = pinStart;
-            mPinLength = pinLength;
-        }
-
-        @Override
-        boolean read(PinRange outPinRange) {
-            outPinRange.start = mPinStart;
-            outPinRange.length = mPinLength;
-            boolean done = mDone;
-            mDone = true;
-            return !done;
-        }
-    }
-
-    private static final class PinRangeSourceStream extends PinRangeSource {
-        private final DataInputStream mStream;
-        private boolean mDone = false;
-
-        PinRangeSourceStream(InputStream stream) {
-            mStream = new DataInputStream(stream);
-        }
-
-        @Override
-        boolean read(PinRange outPinRange) {
-            if (!mDone) {
-                try {
-                    outPinRange.start = mStream.readInt();
-                    outPinRange.length = mStream.readInt();
-                } catch (IOException ex) {
-                    mDone = true;
-                }
-            }
-            return !mDone;
-        }
-    }
-
     /**
      * Helper for pinFile.
      *
@@ -1185,25 +1149,20 @@
      * @return PinnedFile or null on error
      */
     private static PinnedFile pinFileRanges(
-        String fileToPin,
-        int maxBytesToPin,
-        PinRangeSource pinRangeSource)
-    {
+            String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) {
         FileDescriptor fd = new FileDescriptor();
         long address = -1;
-        int mapSize = 0;
+        long mapSize = 0;
 
         try {
             int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC);
             fd = Os.open(fileToPin, openFlags, 0);
             mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
-            address = Os.mmap(0, mapSize,
-                              OsConstants.PROT_READ,
-                              OsConstants.MAP_SHARED,
-                              fd, /*offset=*/0);
+            address = Os.mmap(
+                    0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0);
 
             PinRange pinRange = new PinRange();
-            int bytesPinned = 0;
+            long bytesPinned = 0;
 
             // We pin at page granularity, so make sure the limit is page-aligned
             if (maxBytesToPin % PAGE_SIZE != 0) {
@@ -1211,10 +1170,10 @@
             }
 
             while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
-                int pinStart = pinRange.start;
-                int pinLength = pinRange.length;
-                pinStart = clamp(0, pinStart, mapSize);
-                pinLength = clamp(0, pinLength, mapSize - pinStart);
+                long pinStart = pinRange.start;
+                long pinLength = pinRange.length;
+                pinStart = PinnerUtils.clamp(0, pinStart, mapSize);
+                pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart);
                 pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
 
                 // mlock doesn't require the region to be page-aligned, but we snap the
@@ -1229,14 +1188,13 @@
                 if (pinLength % PAGE_SIZE != 0) {
                     pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
                 }
-                pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
+                pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned);
 
                 if (pinLength > 0) {
                     if (DEBUG) {
                         Slog.d(TAG,
-                               String.format(
-                                   "pinning at %s %s bytes of %s",
-                                   pinStart, pinLength, fileToPin));
+                                String.format("pinning at %s %s bytes of %s", pinStart, pinLength,
+                                        fileToPin));
                     }
                     Os.mlock(address + pinStart, pinLength);
                 }
@@ -1244,15 +1202,15 @@
             }
 
             PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
-            address = -1;  // Ownership transferred
+            address = -1; // Ownership transferred
             return pinnedFile;
         } catch (ErrnoException ex) {
             Slog.e(TAG, "Could not pin file " + fileToPin, ex);
             return null;
         } finally {
-            safeClose(fd);
+            PinnerUtils.safeClose(fd);
             if (address >= 0) {
-                safeMunmap(address, mapSize);
+                PinnerUtils.safeMunmap(address, mapSize);
             }
         }
     }
@@ -1273,81 +1231,50 @@
         }
     }
 
-    public void unpinFile(String filename) {
+    /**
+     * Unpin a file and its optimized dependencies.
+     *
+     * @param filename file to unpin.
+     * @return number of bytes unpinned, 0 in case of failure or nothing to unpin.
+     */
+    public long unpinFile(String filename) {
         PinnedFile pinnedFile;
         synchronized (this) {
             pinnedFile = mPinnedFiles.get(filename);
         }
         if (pinnedFile == null) {
             // File not pinned, nothing to do.
-            return;
+            return 0;
         }
+        long unpinnedBytes = pinnedFile.bytesPinned;
         pinnedFile.close();
         synchronized (this) {
             if (DEBUG) {
                 Slog.d(TAG, "Unpinned file: " + filename);
             }
+            mCurrentPinnedMemory -= pinnedFile.bytesPinned;
+
             mPinnedFiles.remove(pinnedFile.fileName);
             for (PinnedFile dep : pinnedFile.pinnedDeps) {
                 if (dep == null) {
                     continue;
                 }
+                unpinnedBytes -= dep.bytesPinned;
+                mCurrentPinnedMemory -= dep.bytesPinned;
                 mPinnedFiles.remove(dep.fileName);
                 if (DEBUG) {
                     Slog.d(TAG, "Unpinned dependency: " + dep.fileName);
                 }
             }
         }
-    }
 
-    private static int clamp(int min, int value, int max) {
-        return Math.max(min, Math.min(value, max));
-    }
-
-    private static void safeMunmap(long address, long mapSize) {
-        try {
-            Os.munmap(address, mapSize);
-        } catch (ErrnoException ex) {
-            Slog.w(TAG, "ignoring error in unmap", ex);
-        }
-    }
-
-    /**
-     * Close FD, swallowing irrelevant errors.
-     */
-    private static void safeClose(@Nullable FileDescriptor fd) {
-        if (fd != null && fd.valid()) {
-            try {
-                Os.close(fd);
-            } catch (ErrnoException ex) {
-                // Swallow the exception: non-EBADF errors in close(2)
-                // indicate deferred paging write errors, which we
-                // don't care about here. The underlying file
-                // descriptor is always closed.
-                if (ex.errno == OsConstants.EBADF) {
-                    throw new AssertionError(ex);
-                }
-            }
-        }
-    }
-
-    /**
-     * Close closeable thing, swallowing errors.
-     */
-    private static void safeClose(@Nullable Closeable thing) {
-        if (thing != null) {
-            try {
-                thing.close();
-            } catch (IOException ex) {
-                Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
-            }
-        }
+        return unpinnedBytes;
     }
 
     public List<PinnedFileStat> getPinnerStats() {
         ArrayList<PinnedFileStat> stats = new ArrayList<>();
         Collection<PinnedFile> pinnedFiles;
-        synchronized(this) {
+        synchronized (this) {
             pinnedFiles = mPinnedFiles.values();
         }
         for (PinnedFile pf : pinnedFiles) {
@@ -1355,8 +1282,8 @@
             stats.add(stat);
         }
         if (mCurrentlyPinnedAnonSize > 0) {
-            stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME,
-                        mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
+            stats.add(new PinnedFileStat(
+                    ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
         }
         return stats;
     }
@@ -1364,71 +1291,124 @@
     public final class BinderService extends IPinnerService.Stub {
         @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw))
+                return;
             HashSet<PinnedFile> shownPins = new HashSet<>();
-            HashSet<String> groups = new HashSet<>();
-            final int bytesPerMB = 1024 * 1024;
+            HashSet<String> shownGroups = new HashSet<>();
+            HashSet<String> groupsToPrint = new HashSet<>();
+            final double bytesPerMB = 1024 * 1024;
+            pw.format("Pinner Configs:\n");
+            pw.format("   Total Pinner quota: %d%% of total device memory\n",
+                    mConfiguredMaxPinnedMemoryPercentage);
+            pw.format("   Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory,
+                    mConfiguredMaxPinnedMemory / bytesPerMB);
+            pw.format("   Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes);
+            pw.format("\nPinned Files:\n");
             synchronized (PinnerService.this) {
                 long totalSize = 0;
+
+                // We print apps separately from regular pins as they contain extra information that
+                // other pins do not.
                 for (int key : mPinnedApps.keySet()) {
                     PinnedApp app = mPinnedApps.get(key);
                     pw.print(getNameForKey(key));
-                    pw.print(" uid="); pw.print(app.uid);
-                    pw.print(" active="); pw.print(app.active);
+                    pw.print(" uid=");
+                    pw.print(app.uid);
+                    pw.print(" active=");
+                    pw.print(app.active);
+
+                    if (!app.mFiles.isEmpty()) {
+                        shownGroups.add(app.mFiles.getFirst().groupName);
+                    }
                     pw.println();
+                    long bytesPinnedForApp = 0;
+                    long bytesPinnedForAppDeps = 0;
                     for (PinnedFile pf : mPinnedApps.get(key).mFiles) {
                         pw.print("  ");
-                        pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName,
+                        pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName,
                                 pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist);
                         totalSize += pf.bytesPinned;
+                        bytesPinnedForApp += pf.bytesPinned;
                         shownPins.add(pf);
                         for (PinnedFile dep : pf.pinnedDeps) {
                             pw.print("  ");
-                            pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName,
-                                    dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist);
+                            pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+                                    dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+                                    dep.used_pinlist);
                             totalSize += dep.bytesPinned;
+                            bytesPinnedForAppDeps += dep.bytesPinned;
                             shownPins.add(dep);
                         }
                     }
+                    long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps;
+                    pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), "
+                                    + "Dependencies=%d (%.2f MB)]\n\n",
+                            bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB,
+                            bytesPinnedForApp, bytesPinnedForApp / bytesPerMB,
+                            bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB);
                 }
                 pw.println();
                 for (PinnedFile pinnedFile : mPinnedFiles.values()) {
-                    if (!groups.contains(pinnedFile.groupName)) {
-                        groups.add(pinnedFile.groupName);
+                    if (!groupsToPrint.contains(pinnedFile.groupName)
+                            && !shownGroups.contains(pinnedFile.groupName)) {
+                        groupsToPrint.add(pinnedFile.groupName);
                     }
                 }
-                boolean firstPinInGroup = true;
-                for (String group : groups) {
+
+                // Print all the non app groups.
+                for (String group : groupsToPrint) {
                     List<PinnedFile> groupPins = getAllPinsForGroup(group);
+                    pw.print("\nGroup:" + group);
+                    long bytesPinnedForGroupNoDeps = 0;
+                    long bytesPinnedForGroupDeps = 0;
+                    pw.println();
                     for (PinnedFile pinnedFile : groupPins) {
                         if (shownPins.contains(pinnedFile)) {
-                            // Already showed in the dump and accounted for, skip.
+                            // Already displayed and accounted for, skip.
                             continue;
                         }
-                        if (firstPinInGroup) {
-                            firstPinInGroup = false;
-                            // Ensure we only print when there are pins for groups not yet shown
-                            // in the pinned app section.
-                            pw.print("Group:" + group);
-                            pw.println();
-                        }
-                        pw.format("  %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName,
-                                pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB,
-                                pinnedFile.used_pinlist);
+                        pw.format("  %s pinned: %d bytes (%.2f MB) pinlist:%b\n",
+                                pinnedFile.fileName, pinnedFile.bytesPinned,
+                                pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist);
                         totalSize += pinnedFile.bytesPinned;
+                        bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned;
+                        shownPins.add(pinnedFile);
+                        for (PinnedFile dep : pinnedFile.pinnedDeps) {
+                            if (shownPins.contains(dep)) {
+                                // Already displayed and accounted for, skip.
+                                continue;
+                            }
+                            pw.print("  ");
+                            pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+                                    dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+                                    dep.used_pinlist);
+                            totalSize += dep.bytesPinned;
+                            bytesPinnedForGroupDeps += dep.bytesPinned;
+                            shownPins.add(dep);
+                        }
                     }
+                    long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps;
+                    pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), "
+                                    + "Dependencies=%d (%.2f MB)]\n\n",
+                            bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB,
+                            bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB,
+                            bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB);
                 }
                 pw.println();
                 if (mPinAnonAddress != 0) {
-                    pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB);
+                    pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize,
+                            mCurrentlyPinnedAnonSize / bytesPerMB);
                     totalSize += mCurrentlyPinnedAnonSize;
                 }
-                pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB);
+                pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB);
+                pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(),
+                        getAvailableGlobalQuota() / bytesPerMB);
                 pw.println();
                 if (!mPendingRepin.isEmpty()) {
                     pw.print("Pending repin: ");
                     for (int key : mPendingRepin.values()) {
-                        pw.print(getNameForKey(key)); pw.print(' ');
+                        pw.print(getNameForKey(key));
+                        pw.print(' ');
                     }
                     pw.println();
                 }
@@ -1462,8 +1442,9 @@
                     repin();
                     break;
                 default:
-                    printError(out, String.format(
-                            "Unknown pinner command: %s. Supported commands: repin", command));
+                    printError(out,
+                            String.format("Unknown pinner command: %s. Supported commands: repin",
+                                    command));
                     resultReceiver.send(-1, null);
                     return;
             }
@@ -1479,46 +1460,6 @@
         }
     }
 
-    @VisibleForTesting
-    public static final class PinnedFile implements AutoCloseable {
-        private long mAddress;
-        final int mapSize;
-        final String fileName;
-        final int bytesPinned;
-
-        // Whether this file was pinned using a pinlist
-        boolean used_pinlist;
-
-        // User defined group name for pinner accounting
-        String groupName = "";
-        ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
-
-        PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
-             mAddress = address;
-             this.mapSize = mapSize;
-             this.fileName = fileName;
-             this.bytesPinned = bytesPinned;
-        }
-
-        @Override
-        public void close() {
-            if (mAddress >= 0) {
-                safeMunmap(mAddress, mapSize);
-                mAddress = -1;
-            }
-            for (PinnedFile dep : pinnedDeps) {
-                if (dep != null) {
-                    dep.close();
-                }
-            }
-        }
-
-        @Override
-        public void finalize() {
-            close();
-        }
-    }
-
     final static class PinRange {
         int start;
         int length;
@@ -1528,7 +1469,6 @@
      * Represents an app that was pinned.
      */
     private final class PinnedApp {
-
         /**
          * The uid of the package being pinned. This stays constant while the package stays
          * installed.
@@ -1557,11 +1497,9 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case PIN_ONSTART_MSG:
-                {
+                case PIN_ONSTART_MSG: {
                     handlePinOnStart();
-                }
-                break;
+                } break;
 
                 default:
                     super.handleMessage(msg);
diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java
new file mode 100644
index 0000000..a836a83
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnerUtils.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+import android.annotation.Nullable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/* package */ final class PinnerUtils {
+    private static final String TAG = "PinnerUtils";
+
+    public static long clamp(long min, long value, long max) {
+        return Math.max(min, Math.min(value, max));
+    }
+
+    public static void safeMunmap(long address, long mapSize) {
+        try {
+            Os.munmap(address, mapSize);
+        } catch (ErrnoException ex) {
+            Slog.w(TAG, "ignoring error in unmap", ex);
+        }
+    }
+
+    /**
+     * Close FD, swallowing irrelevant errors.
+     */
+    public static void safeClose(@Nullable FileDescriptor fd) {
+        if (fd != null && fd.valid()) {
+            try {
+                Os.close(fd);
+            } catch (ErrnoException ex) {
+                // Swallow the exception: non-EBADF errors in close(2)
+                // indicate deferred paging write errors, which we
+                // don't care about here. The underlying file
+                // descriptor is always closed.
+                if (ex.errno == OsConstants.EBADF) {
+                    throw new AssertionError(ex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Close closeable thing, swallowing errors.
+     */
+    public static void safeClose(@Nullable Closeable thing) {
+        if (thing != null) {
+            try {
+                thing.close();
+            } catch (IOException ex) {
+                Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 9ecc7b9..1569fa0 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -70,13 +70,13 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
 import com.android.server.art.ArtManagerLocal;
 import com.android.server.art.DexUseManagerLocal;
 import com.android.server.art.ReasonMapping;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DexoptParams;
 import com.android.server.art.model.DexoptResult;
+import com.android.server.pinner.PinnerService;
 import com.android.server.pm.PackageDexOptimizer.DexOptResult;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
index f047f56..ab630ee 100644
--- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
@@ -17,6 +17,7 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -63,6 +64,7 @@
     private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger";
     private static final String SUBSYSTEM_SENSOR_STRING = "Sensor";
     private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data";
+    private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth";
     private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution";
 
     private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
@@ -512,6 +514,8 @@
                 return CPU_WAKEUP_SUBSYSTEM_SENSOR;
             case SUBSYSTEM_CELLULAR_DATA_STRING:
                 return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+            case SUBSYSTEM_BLUETOOTH_STRING:
+                return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
         }
         return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
     }
@@ -528,6 +532,8 @@
                 return SUBSYSTEM_SENSOR_STRING;
             case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
                 return SUBSYSTEM_CELLULAR_DATA_STRING;
+            case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH:
+                return SUBSYSTEM_BLUETOOTH_STRING;
             case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
                 return "Unknown";
         }
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index b35a0a7..74c1124e 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -216,13 +216,13 @@
 import com.android.server.BinderCallsStatsService;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
-import com.android.server.PinnerService.PinnedFileStats;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.am.MemoryStatUtil.MemoryStat;
 import com.android.server.health.HealthServiceWrapper;
 import com.android.server.notification.NotificationManagerService;
+import com.android.server.pinner.PinnerService;
+import com.android.server.pinner.PinnerService.PinnedFileStats;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.power.stats.KernelWakelockReader;
 import com.android.server.power.stats.KernelWakelockStats;
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index c120fc7..6aed00e 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -57,8 +57,7 @@
     // for a snippet of the current known vibrator state/info.
     private volatile VibratorInfo mVibratorInfo;
     private volatile boolean mVibratorInfoLoadSuccessful;
-    private volatile boolean mIsVibrating;
-    private volatile boolean mIsUnderExternalControl;
+    private volatile VibratorState mCurrentState;
     private volatile float mCurrentAmplitude;
 
     /**
@@ -75,6 +74,11 @@
         void onComplete(int vibratorId, long vibrationId);
     }
 
+    /** Representation of the vibrator state based on the interactions through this controller. */
+    private enum VibratorState {
+        IDLE, VIBRATING, UNDER_EXTERNAL_CONTROL
+    }
+
     VibratorController(int vibratorId, OnVibrationCompleteListener listener) {
         this(vibratorId, listener, new NativeWrapper());
     }
@@ -87,6 +91,7 @@
         VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId);
         mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder);
         mVibratorInfo = vibratorInfoBuilder.build();
+        mCurrentState = VibratorState.IDLE;
 
         if (!mVibratorInfoLoadSuccessful) {
             Slog.e(TAG,
@@ -106,7 +111,7 @@
                     return false;
                 }
                 // Notify its callback after new client registered.
-                notifyStateListener(listener, mIsVibrating);
+                notifyStateListener(listener, isVibrating(mCurrentState));
             }
             return true;
         } finally {
@@ -166,7 +171,7 @@
      * automatically notified to any registered {@link IVibratorStateListener} on change.
      */
     public boolean isVibrating() {
-        return mIsVibrating;
+        return isVibrating(mCurrentState);
     }
 
     /**
@@ -184,11 +189,6 @@
         return mCurrentAmplitude;
     }
 
-    /** Return {@code true} if this vibrator is under external control, false otherwise. */
-    public boolean isUnderExternalControl() {
-        return mIsUnderExternalControl;
-    }
-
     /**
      * Check against this vibrator capabilities.
      *
@@ -214,7 +214,7 @@
     /**
      * Set the vibrator control to be external or not, based on given flag.
      *
-     * <p>This will affect the state of {@link #isUnderExternalControl()}.
+     * <p>This will affect the state of {@link #isVibrating()}.
      */
     public void setExternalControl(boolean externalControl) {
         Trace.traceBegin(TRACE_TAG_VIBRATOR,
@@ -224,9 +224,11 @@
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
                 return;
             }
+            VibratorState newState =
+                    externalControl ? VibratorState.UNDER_EXTERNAL_CONTROL : VibratorState.IDLE;
             synchronized (mLock) {
-                mIsUnderExternalControl = externalControl;
                 mNativeWrapper.setExternalControl(externalControl);
+                updateStateAndNotifyListenersLocked(newState);
             }
         } finally {
             Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -264,7 +266,7 @@
                 if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
                     mNativeWrapper.setAmplitude(amplitude);
                 }
-                if (mIsVibrating) {
+                if (mCurrentState == VibratorState.VIBRATING) {
                     mCurrentAmplitude = amplitude;
                 }
             }
@@ -289,7 +291,7 @@
                 long duration = mNativeWrapper.on(milliseconds, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
@@ -319,7 +321,7 @@
                         vendorEffect.getAdaptiveScale(), vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             } finally {
@@ -346,7 +348,7 @@
                         prebaked.getEffectStrength(), vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
@@ -374,7 +376,7 @@
                 long duration = mNativeWrapper.compose(primitives, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
@@ -402,7 +404,7 @@
                 long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId);
                 if (duration > 0) {
                     mCurrentAmplitude = -1;
-                    notifyListenerOnVibrating(true);
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
                 }
                 return duration;
             }
@@ -422,7 +424,7 @@
             synchronized (mLock) {
                 mNativeWrapper.off();
                 mCurrentAmplitude = 0;
-                notifyListenerOnVibrating(false);
+                updateStateAndNotifyListenersLocked(VibratorState.IDLE);
             }
         } finally {
             Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -443,9 +445,8 @@
         return "VibratorController{"
                 + "mVibratorInfo=" + mVibratorInfo
                 + ", mVibratorInfoLoadSuccessful=" + mVibratorInfoLoadSuccessful
-                + ", mIsVibrating=" + mIsVibrating
+                + ", mCurrentState=" + mCurrentState.name()
                 + ", mCurrentAmplitude=" + mCurrentAmplitude
-                + ", mIsUnderExternalControl=" + mIsUnderExternalControl
                 + ", mVibratorStateListeners count="
                 + mVibratorStateListeners.getRegisteredCallbackCount()
                 + '}';
@@ -454,8 +455,7 @@
     void dump(IndentingPrintWriter pw) {
         pw.println("Vibrator (id=" + mVibratorInfo.getId() + "):");
         pw.increaseIndent();
-        pw.println("isVibrating = " + mIsVibrating);
-        pw.println("isUnderExternalControl = " + mIsUnderExternalControl);
+        pw.println("currentState = " + mCurrentState.name());
         pw.println("currentAmplitude = " + mCurrentAmplitude);
         pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful);
         pw.println("vibratorStateListener size = "
@@ -464,14 +464,19 @@
         pw.decreaseIndent();
     }
 
+    /**
+     * Updates current vibrator state and notify listeners if {@link #isVibrating()} result changed.
+     */
     @GuardedBy("mLock")
-    private void notifyListenerOnVibrating(boolean isVibrating) {
-        if (mIsVibrating != isVibrating) {
-            mIsVibrating = isVibrating;
+    private void updateStateAndNotifyListenersLocked(VibratorState state) {
+        boolean previousIsVibrating = isVibrating(mCurrentState);
+        final boolean newIsVibrating = isVibrating(state);
+        mCurrentState = state;
+        if (previousIsVibrating != newIsVibrating) {
             // The broadcast method is safe w.r.t. register/unregister listener methods, but lock
             // is required here to guarantee delivery order.
             mVibratorStateListeners.broadcast(
-                    listener -> notifyStateListener(listener, isVibrating));
+                    listener -> notifyStateListener(listener, newIsVibrating));
         }
     }
 
@@ -483,6 +488,11 @@
         }
     }
 
+    /** Returns true only if given state is not {@link VibratorState#IDLE}. */
+    private static boolean isVibrating(VibratorState state) {
+        return state != VibratorState.IDLE;
+    }
+
     /** Wrapper around the static-native methods of {@link VibratorController} for tests. */
     @VisibleForTesting
     public static class NativeWrapper {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 95c6483..07473d1 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -809,17 +809,9 @@
                 mCurrentExternalVibration.getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION);
             }
-
-            boolean isVibrating = false;
-            boolean isUnderExternalControl = false;
             for (int i = 0; i < mVibrators.size(); i++) {
                 proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i));
-                isVibrating |= mVibrators.valueAt(i).isVibrating();
-                isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl();
             }
-            proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating);
-            proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL,
-                    isUnderExternalControl);
         }
         mVibratorManagerRecords.dump(proto);
         mVibratorControlService.dump(proto);
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 6740153..ab5316f 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -41,7 +41,8 @@
 
 import com.android.internal.util.XmlUtils;
 import com.android.server.LocalServices;
-import com.android.server.PinnerService;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -318,8 +319,9 @@
             if (webviewPinQuota <= 0) {
                 break;
             }
-            int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP);
-            webviewPinQuota -= bytesPinned;
+            PinnedFile pf = pinnerService.pinFile(
+                    apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true);
+            webviewPinQuota -= pf.bytesPinned;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index 241390c1..fbf9478 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -170,7 +170,7 @@
      * </ul>
      */
     boolean shouldApplyFreeformTreatmentForCameraCompat() {
-        return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord,
+        return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord,
                 OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 67bfd76..5338c01 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -48,8 +48,9 @@
         // without the need to restart the device.
         final boolean needsDisplayRotationCompatPolicy =
                 wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
-        final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
-                && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
+        final boolean needsCameraCompatFreeformPolicy =
+                Flags.enableCameraCompatForDesktopWindowing()
+                        && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
         if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
             mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
             mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2259b5a..515f148 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -1120,7 +1120,9 @@
             @Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
             int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) {
         // BAL Exception allowed in all cases
-        if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
+        if (balCode == BAL_ALLOW_ALLOWLISTED_UID
+                || (android.security.Flags.asmReintroduceGracePeriod()
+                    && balCode == BAL_ALLOW_GRACE_PERIOD)) {
             return true;
         }
 
@@ -1173,10 +1175,15 @@
                 ArrayList<Task> visibleTasks = displayArea.getVisibleTasks();
                 for (int i = 0; i < visibleTasks.size(); i++) {
                     Task task = visibleTasks.get(i);
-                    if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
-                        bas.optedIn(task.getTopMostActivity());
-                    } else {
+                    if (android.security.Flags.asmReintroduceGracePeriod()) {
                         bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas);
+                    } else {
+                        if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
+                            bas.optedIn(task.getTopMostActivity());
+                        } else {
+                            bas = checkTopActivityForAsm(
+                                task, callingUid, /*sourceRecord*/null, bas);
+                        }
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index e3232e0..d6caa1a 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -124,7 +124,7 @@
      */
     @VisibleForTesting
     boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
-        return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled(
+        return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
                 ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
     }
 
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index ddbfd70..d7dc459 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -222,7 +222,8 @@
             UserHandle clientUser) {
         final InputConsumerImpl existingConsumer = getInputConsumer(name);
         if (existingConsumer != null && existingConsumer.mClientUser.equals(clientUser)) {
-            throw new IllegalStateException("Existing input consumer found with name: " + name
+            destroyInputConsumer(existingConsumer.mToken);
+            Slog.w(TAG_WM, "Replacing existing input consumer found with name: " + name
                     + ", display: " + mDisplayId + ", user: " + clientUser);
         }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 86bb75a..14f034b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -66,6 +66,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.PAUSING;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -6177,6 +6178,8 @@
 
     void maybeApplyLastRecentsAnimationTransaction() {
         if (mLastRecentsAnimationTransaction != null) {
+            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+                    "Applying last recents animation transaction.");
             final SurfaceControl.Transaction tx = getPendingTransaction();
             if (mLastRecentsAnimationOverlay != null) {
                 tx.reparent(mLastRecentsAnimationOverlay, mSurfaceControl);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b8f47cc..9426347 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9007,7 +9007,9 @@
 
         final boolean isInputTargetNotFocused =
                 mFocusedInputTarget != t && mFocusedInputTarget != null;
-        if (!isInputTargetNotFocused) {
+        final boolean isTouchOnFocusedDisplay = mFocusedInputTarget != null
+                && t.getDisplayId() == mFocusedInputTarget.getDisplayId();
+        if (!(isInputTargetNotFocused && isTouchOnFocusedDisplay)) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 476443a..f35f2b3 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -799,7 +799,12 @@
             }
         } finally {
             if (deferTransitionReady) {
-                chain.mTransition.continueTransitionReady();
+                if (chain.mTransition.isCollecting()) {
+                    chain.mTransition.continueTransitionReady();
+                } else {
+                    Slog.wtf(TAG, "Too late, transition : " + chain.mTransition.getSyncId()
+                            + " state: " + chain.mTransition.getState() + " is not collecting");
+                }
             }
             mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
             if (deferResume) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4c4b4f6..0e3ab63 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1969,6 +1969,13 @@
     boolean isReadyForDisplay() {
         final boolean parentAndClientVisible = !isParentWindowHidden()
                 && mViewVisibility == View.VISIBLE;
+        // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to
+        // shell transitions
+        if (!mTransitionController.isShellTransitionsEnabled()) {
+            return mHasSurface && isVisibleByPolicy() && !mDestroying
+                    && ((parentAndClientVisible && mToken.isVisible())
+                    || isAnimating(TRANSITION | PARENTS));
+        }
         return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible()
                 && (parentAndClientVisible || isAnimating(TRANSITION | PARENTS));
     }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 5cf260a..ce6f1ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -205,6 +205,7 @@
 import com.android.server.pdb.PersistentDataBlockService;
 import com.android.server.people.PeopleService;
 import com.android.server.permission.access.AccessCheckingService;
+import com.android.server.pinner.PinnerService;
 import com.android.server.pm.ApexManager;
 import com.android.server.pm.ApexSystemServiceInfo;
 import com.android.server.pm.BackgroundInstallControlService;
diff --git a/services/tests/RemoteProvisioningServiceTests/Android.bp b/services/tests/RemoteProvisioningServiceTests/Android.bp
index 19c9136..3a73c39 100644
--- a/services/tests/RemoteProvisioningServiceTests/Android.bp
+++ b/services/tests/RemoteProvisioningServiceTests/Android.bp
@@ -31,7 +31,6 @@
         "service-rkp.impl",
         "services.core",
         "truth",
-        "truth-java8-extension",
     ],
     test_suites: [
         "device-tests",
diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
index 007c0db..a1616c6 100644
--- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
+++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
@@ -17,7 +17,6 @@
 package com.android.server.security.rkp;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
diff --git a/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt
new file mode 100644
index 0000000..413eb31
--- /dev/null
+++ b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appfunctions
+
+import android.app.appsearch.GenericDocument
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+
+@RunWith(JUnit4::class)
+class GenericDocumentWrapperTest {
+
+    @Test
+    fun parcelUnparcel() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+
+        val recovered = parcelUnparcel(wrapper)
+
+        assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+    @Test
+    fun parcelUnparcel_afterGetValue() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+        assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42)
+
+        val recovered = parcelUnparcel(wrapper)
+
+        assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+
+    @Test
+    fun getValue() {
+        val doc =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("test", 42)
+                .build()
+        val wrapper = GenericDocumentWrapper(doc)
+
+        assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42)
+    }
+
+    private fun parcelUnparcel(obj: GenericDocumentWrapper): GenericDocumentWrapper {
+        val parcel = Parcel.obtain()
+        try {
+            obj.writeToParcel(parcel, 0)
+            parcel.setDataPosition(0)
+            return GenericDocumentWrapper.CREATOR.createFromParcel(parcel)
+        } finally {
+            parcel.recycle()
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
index fd55428..c3df078 100644
--- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
+++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
@@ -32,4 +32,7 @@
     <device name="test.sensor.device">
         <subsystem>Sensor</subsystem>
     </device>
+    <device name="test.bluetooth.device">
+        <subsystem>Bluetooth</subsystem>
+    </device>
 </irq-device-map>
\ No newline at end of file
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
index 0dc836b..fe4d971 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
@@ -17,6 +17,7 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -52,6 +53,7 @@
     private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device";
     private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device";
     private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device";
+    private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device";
     private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
     private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device";
     private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device";
@@ -62,12 +64,14 @@
     private static final int TEST_UID_3 = 92261423;
     private static final int TEST_UID_4 = 56926423;
     private static final int TEST_UID_5 = 76421423;
+    private static final int TEST_UID_6 = 62345353;
 
     private static final int TEST_PROC_STATE_1 = 72331;
     private static final int TEST_PROC_STATE_2 = 792351;
     private static final int TEST_PROC_STATE_3 = 138831;
     private static final int TEST_PROC_STATE_4 = 23231;
     private static final int TEST_PROC_STATE_5 = 42;
+    private static final int TEST_PROC_STATE_6 = 129942;
 
     private static final Context sContext = InstrumentationRegistry.getTargetContext();
     private final Handler mHandler = Mockito.mock(Handler.class);
@@ -79,6 +83,7 @@
         obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3);
         obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4);
         obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5);
+        obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6);
     }
 
     @Test
@@ -118,6 +123,7 @@
                 CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
                 CPU_WAKEUP_SUBSYSTEM_SENSOR,
                 CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+                CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
         };
 
         final String[] kernelReasons = new String[] {
@@ -126,10 +132,11 @@
                 KERNEL_REASON_SOUND_TRIGGER_IRQ,
                 KERNEL_REASON_SENSOR_IRQ,
                 KERNEL_REASON_CELLULAR_DATA_IRQ,
+                KERNEL_REASON_BLUETOOTH_IRQ,
         };
 
         final int[] uids = new int[] {
-                TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5
+                TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6
         };
 
         final int[] procStates = new int[] {
@@ -137,7 +144,8 @@
                 TEST_PROC_STATE_3,
                 TEST_PROC_STATE_4,
                 TEST_PROC_STATE_1,
-                TEST_PROC_STATE_5
+                TEST_PROC_STATE_5,
+                TEST_PROC_STATE_6
         };
 
         final int total = subsystems.length;
@@ -285,6 +293,40 @@
     }
 
     @Test
+    public void bluetoothIrqAttributionSolo() {
+        final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+        final long wakeupTime = 1236121;
+
+        populateDefaultProcStates(obj);
+
+        obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ);
+
+        // Outside the window, so should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2);
+        // Should be attributed
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3,
+                TEST_UID_5);
+
+        final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+        assertThat(attribution).isNotNull();
+        assertThat(attribution.size()).isEqualTo(1);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_1)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_2)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+                TEST_PROC_STATE_3);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+                TEST_UID_4)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+                TEST_PROC_STATE_5);
+    }
+
+    @Test
     public void alarmAndWifiIrqAttribution() {
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
         final long wakeupTime = 92123210;
@@ -400,6 +442,47 @@
     }
 
     @Test
+    public void unknownAndBluetoothAttribution() {
+        final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+        final long wakeupTime = 92123520;
+
+        populateDefaultProcStates(obj);
+
+        obj.noteWakeupTimeAndReason(wakeupTime, 24,
+                KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ);
+
+        // Bluetooth activity
+        // Outside the window, so should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+                wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4);
+        // Should be attributed
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3,
+                TEST_UID_5);
+
+        // Unrelated, should be ignored.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+
+        final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+        assertThat(attribution).isNotNull();
+        assertThat(attribution.size()).isEqualTo(2);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo(
+                TEST_PROC_STATE_1);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+                .indexOfKey(TEST_UID_2)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+                TEST_PROC_STATE_3);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+                .indexOfKey(TEST_UID_4)).isLessThan(0);
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+                TEST_PROC_STATE_5);
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+        assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull();
+        assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse();
+    }
+
+    @Test
     public void unknownFormatWakeupIgnored() {
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
         final long wakeupTime = 72123210;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ac1b7c6..cbe6700 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -52,6 +52,7 @@
         "services.credentials",
         "services.devicepolicy",
         "services.flags",
+        "com.android.server.flags.services-aconfig-java",
         "services.net",
         "services.people",
         "services.supervision",
@@ -81,6 +82,7 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
+        "flag-junit",
         "junit",
         "junit-params",
         "ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
index ec78bce..c18faef 100644
--- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
@@ -31,6 +31,9 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.testing.TestableContext;
@@ -43,6 +46,9 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.flags.Flags;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
 import com.android.server.testutils.FakeDeviceConfigInterface;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
@@ -73,15 +79,18 @@
 
     private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
 
+    private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10;
+
     @Rule
     public TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private final ArraySet<String> mUpdatedPackages = new ArraySet<>();
     private ResolveInfo mHomePackageResolveInfo;
     private FakeDeviceConfigInterface mFakeDeviceConfigInterface;
     private PinnerService.Injector mInjector;
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -114,6 +123,8 @@
         resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false);
         resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0);
         resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false);
+        resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage,
+                MEMORY_PERCENTAGE_FOR_QUOTA);
 
         mFakeDeviceConfigInterface = new FakeDeviceConfigInterface();
         setDeviceConfigPinnedAnonSize(0);
@@ -138,10 +149,9 @@
             }
 
             @Override
-            protected PinnerService.PinnedFile pinFileInternal(String fileToPin,
-                    int maxBytesToPin, boolean attemptPinIntrospection) {
-                return new PinnerService.PinnedFile(-1,
-                        maxBytesToPin, fileToPin, maxBytesToPin);
+            protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+                    long maxBytesToPin, boolean attemptPinIntrospection) {
+                return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin);
             }
         };
     }
@@ -167,6 +177,12 @@
         unpinAnonRegionMethod.invoke(pinnerService);
     }
 
+    private long getGlobalPinQuota(PinnerService service) throws Exception {
+        Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota");
+        getQuotaMethod.setAccessible(true);
+        return (long) getQuotaMethod.invoke(service);
+    }
+
     private void waitForPinnerService(PinnerService pinnerService)
             throws NoSuchFieldException, IllegalAccessException {
         // There's no notification/callback when pinning finished
@@ -315,15 +331,121 @@
         PinnerService pinnerService = new PinnerService(mContext, mInjector);
         pinnerService.onStart();
 
-        pinnerService.pinFile("test_file", 4096, null, "my_group");
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
 
-        assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
-        assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0);
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(4096);
+        assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1);
 
         unpinAll(pinnerService);
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testPinAllQuota() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        long quota = getGlobalPinQuota(pinnerService);
+
+        pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false);
+
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(quota);
+
+        unpinAll(pinnerService);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinQuotaAsDevicePercentage() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+        long origQuota = getGlobalPinQuota(pinnerService);
+
+        long totalMem = android.os.Process.getTotalMemory();
+
+        // Verify that pin quota is the set percentage of device total memory
+        assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100);
+
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinWhenNoQuota() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // Verify that pin quota is zero
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+
+        pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+        assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0);
+    }
+
+    /**
+     * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix
+     * flag.
+     */
+    @Test
+    @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalQuotaDisabled() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // The quota parameter exists but it should have no effect on pinning
+        long quota = getGlobalPinQuota(pinnerService);
+
+        pinnerService.pinFile("test_file", quota + 1, null, "my_group", false);
+
+        // Verify that we can pin past the quota as it is disabled
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testUnpinReleasesQuota() throws Exception {
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+        long origQuota = getGlobalPinQuota(pinnerService);
+
+        // Verify that pin quota exists and is non zero.
+        assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0);
+
+        pinnerService.pinFile("test_file", origQuota, null, "my_group", false);
+
+        // Make sure all the quota was consumed
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota);
+
+        // Unpin the file and verify that the quota has been released.
+        pinnerService.unpinFile("test_file");
+        assertThat(getPinnedSize(pinnerService)).isEqualTo(0);
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+    public void testGlobalPinQuotaNegative() throws Exception {
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(
+                com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10);
+
+        PinnerService pinnerService = new PinnerService(mContext, mInjector);
+        pinnerService.onStart();
+
+        // Verify that pin quota is zero
+        assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+    }
+
+    @Test
     public void testPinAnonRegion() throws Exception {
         setDeviceConfigPinnedAnonSize(32768);
 
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index abc9ce3..ee63d5d 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -91,6 +92,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -174,8 +176,8 @@
     private PackageManager mPackageManager;
     @Mock
     private KeyguardManager mKeyguardManager;
-    @Mock
-    AppOpsManager mAppOpsManager;
+
+    private AppOpsManager mAppOpsManager;
     @Mock
     private IMediaProjectionWatcherCallback mWatcherCallback;
     @Mock
@@ -193,6 +195,7 @@
         LocalServices.removeServiceForTest(WindowManagerInternal.class);
         LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
 
+        mAppOpsManager = mockAppOpsManager();
         mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
         mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
         mContext.setMockPackageManager(mPackageManager);
@@ -206,6 +209,17 @@
         mService = new MediaProjectionManagerService(mContext);
     }
 
+    private static AppOpsManager mockAppOpsManager() {
+        return mock(AppOpsManager.class, invocationOnMock -> {
+            if (invocationOnMock.getMethod().getName().startsWith("noteOp")) {
+                // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED
+                // and is not what we want.
+                return AppOpsManager.MODE_IGNORED;
+            }
+            return Answers.RETURNS_DEFAULTS.answer(invocationOnMock);
+        });
+    }
+
     @After
     public void tearDown() {
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
@@ -305,8 +319,10 @@
     public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
             throws NameNotFoundException {
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
-        doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA),
-                eq(projection.uid), eq(projection.packageName));
+        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager)
+                .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA),
+                        eq(projection.uid), eq(projection.packageName), nullable(String.class),
+                        nullable(String.class));
         doReturn(true).when(mKeyguardManager).isKeyguardLocked();
 
         doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
@@ -1159,7 +1175,7 @@
         doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
                 any(ApplicationInfoFlags.class), any(UserHandle.class));
         return service.createProjectionInternal(UID, PACKAGE_NAME,
-                TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
+                TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT);
     }
 
     // Set up preconditions for starting a projection, with no foreground service requirements.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6c9015d..44770d2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -4652,7 +4652,42 @@
 
         doThrow(new SecurityException("no access")).when(mUgmInternal)
                 .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri),
-                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+                anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        mBinderService.updateNotificationChannelFromPrivilegedListener(
+                null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
+
+        verify(mPreferencesHelper, times(1)).updateNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(),  anyInt(), anyBoolean());
+
+        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
+                eq(Process.myUserHandle()), eq(mTestNotificationChannel),
+                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+    }
+
+    @Test
+    public void
+        testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
+            throws Exception {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(singletonList(mock(AssociationInfo.class)));
+        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
+                eq(mTestNotificationChannel.getId()), anyBoolean()))
+                .thenReturn(mTestNotificationChannel);
+
+        // Missing Uri permissions for the old channel sound
+        final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri),
+                anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        // Has Uri permissions for the old channel sound
+        final Uri newSoundUri = Uri.parse("content://media/test/sound/uri");
+        final NotificationChannel updatedNotificationChannel = new NotificationChannel(
+                TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+        updatedNotificationChannel.setSound(newSoundUri,
+                updatedNotificationChannel.getAudioAttributes());
 
         mBinderService.updateNotificationChannelFromPrivilegedListener(
                 null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index a0c0df8..d64b9e8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -45,11 +45,13 @@
 import static android.app.NotificationManager.IMPORTANCE_NONE;
 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
 import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE;
+import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
 import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_SYSTEM;
-
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
 import static android.service.notification.Flags.notificationClassification;
@@ -59,6 +61,7 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
 import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI;
 import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
 import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
 import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
@@ -84,6 +87,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -369,10 +373,10 @@
 
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         resetZenModeHelper();
 
         mAudioAttributes = new AudioAttributes.Builder()
@@ -783,7 +787,7 @@
     public void testReadXml_oldXml_migrates() throws Exception {
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"2\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1
@@ -919,7 +923,7 @@
     public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -978,7 +982,7 @@
     public void testReadXml_newXml_permissionNotificationOff() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ false, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1037,7 +1041,7 @@
     public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                /* showReviewPermissionsNotification= */ true, mClock);
+                mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
         String xml = "<ranking version=\"4\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1709,7 +1713,7 @@
         // simulate load after reboot
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
 
         // Trigger 2nd restore pass
@@ -1764,7 +1768,7 @@
         // simulate load after reboot
         mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         loadByteArrayXml(xml.getBytes(), false, USER_ALL);
 
         // Trigger 2nd restore pass
@@ -1842,10 +1846,10 @@
 
         mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
         mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
-                false, mClock);
+                mUgmInternal, false, mClock);
 
         NotificationChannel channel =
                 new NotificationChannel("id", "name", IMPORTANCE_LOW);
@@ -3049,6 +3053,64 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() {
+        final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        assertThrows(SecurityException.class,
+                () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel,
+                    true, false, UID_N_MR1, false));
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true))
+                .isNull();
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() {
+        final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+                .getSound()).isEqualTo(sound);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+    public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() {
+        final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound");
+
+        doThrow(new SecurityException("no access")).when(mUgmInternal)
+                .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+        final NotificationChannel channel = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setSound(sound, mAudioAttributes);
+
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+        assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+                .getSound()).isEqualTo(sound);
+    }
+
+    @Test
     public void testPermanentlyDeleteChannels() throws Exception {
         NotificationChannel channel1 =
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
index 0d13be6..e8ca8bf 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -127,13 +127,13 @@
     public void setExternalControl_withCapability_enablesExternalControl() {
         mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
         VibratorController controller = createController();
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         controller.setExternalControl(true);
-        assertTrue(controller.isUnderExternalControl());
+        assertTrue(controller.isVibrating());
 
         controller.setExternalControl(false);
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
         inOrderVerifier.verify(mNativeWrapperMock).setExternalControl(eq(true));
@@ -143,10 +143,10 @@
     @Test
     public void setExternalControl_withNoCapability_ignoresExternalControl() {
         VibratorController controller = createController();
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         controller.setExternalControl(true);
-        assertFalse(controller.isUnderExternalControl());
+        assertFalse(controller.isVibrating());
 
         verify(mNativeWrapperMock, never()).setExternalControl(anyBoolean());
     }
@@ -181,6 +181,38 @@
     }
 
     @Test
+    public void setAmplitude_vibratorIdle_ignoresAmplitude() {
+        VibratorController controller = createController();
+        assertFalse(controller.isVibrating());
+
+        controller.setAmplitude(1);
+        assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
+    public void setAmplitude_vibratorUnderExternalControl_ignoresAmplitude() {
+        mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorController controller = createController();
+        controller.setExternalControl(true);
+        assertTrue(controller.isVibrating());
+
+        controller.setAmplitude(1);
+        assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
+    public void setAmplitude_vibratorVibrating_setsAmplitude() {
+        when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
+        VibratorController controller = createController();
+        controller.on(100, /* vibrationId= */ 1);
+        assertTrue(controller.isVibrating());
+        assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0);
+
+        controller.setAmplitude(1);
+        assertEquals(1, controller.getCurrentAmplitude(), /* delta= */ 0);
+    }
+
+    @Test
     public void on_withDuration_turnsVibratorOn() {
         when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
         VibratorController controller = createController();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index d99b20c6..538c3fc 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -266,12 +266,13 @@
     @After
     public void tearDown() throws Exception {
         if (mService != null) {
-            if (!mPendingVibrations.stream().allMatch(HalVibration::hasEnded)) {
-                // Cancel any pending vibration from tests.
-                cancelVibrate(mService);
-                for (HalVibration vibration : mPendingVibrations) {
-                    vibration.waitForEnd();
-                }
+            // Make sure we have permission to cancel test vibrations, even if the test denied them.
+            grantPermission(android.Manifest.permission.VIBRATE);
+            // Cancel any pending vibration from tests, including external vibrations.
+            cancelVibrate(mService);
+            // Wait until pending vibrations end asynchronously.
+            for (HalVibration vibration : mPendingVibrations) {
+                vibration.waitForEnd();
             }
             // Wait until all vibrators have stopped vibrating, waiting for ramp-down.
             // Note: if a test is flaky here something is wrong with the vibration finalization.
@@ -2242,7 +2243,7 @@
         VibratorManagerService service = createSystemReadyService();
 
         VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100);
-        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+        HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
 
         // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2255,7 +2256,8 @@
         assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
 
         // Vibration is cancelled.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        vibration.waitForEnd();
+        assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
         assertEquals(Arrays.asList(false, true),
                 mVibratorProviders.get(1).getExternalControlStates());
     }
@@ -2296,7 +2298,7 @@
 
         VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
                 new long[]{100, 200, 300}, new int[]{128, 255, 255}, 1);
-        vibrate(service, repeatingEffect, ALARM_ATTRS);
+        HalVibration repeatingVibration = vibrate(service, repeatingEffect, ALARM_ATTRS);
 
         // VibrationThread will start this vibration async, so wait until vibration is triggered.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2308,7 +2310,8 @@
         assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
 
         // Vibration is cancelled.
-        assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+        repeatingVibration.waitForEnd();
+        assertThat(repeatingVibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
         assertEquals(Arrays.asList(false, true),
                 mVibratorProviders.get(1).getExternalControlStates());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index d66c21a..b91a5b7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -28,7 +28,7 @@
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import android.compat.testing.PlatformCompatChangeRule;
 import android.platform.test.annotations.DisableFlags;
@@ -218,7 +218,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -229,7 +229,7 @@
 
     @Test
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -240,7 +240,7 @@
 
     @Test
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
@@ -250,7 +250,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
         runTestScenario((robot) -> {
             robot.activity().createActivityWithComponentInNewTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index d91b38e..41102d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,7 +20,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -85,7 +85,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -95,7 +95,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -105,7 +105,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -115,7 +115,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -125,7 +125,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -136,7 +136,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_existsWhenCameraCompatFreeformExists() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(true);
@@ -147,7 +147,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_startedWhenCameraCompatFreeformExists() {
         runTestScenario((robot) -> {
             robot.allowEnterDesktopMode(true);
@@ -180,7 +180,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testCameraStateManager_doesNotExistWhenNoPolicyExists() {
         runTestScenario((robot) -> {
             robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index a48813d..dbcef10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -35,7 +35,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -60,6 +60,7 @@
 import android.graphics.Rect;
 import android.hardware.camera2.CameraManager;
 import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.DisplayInfo;
 import android.view.Surface;
@@ -135,7 +136,6 @@
                 });
 
         mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler);
-        mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM);
         CameraStateMonitor cameraStateMonitor =
                 new CameraStateMonitor(mDisplayContent, mMockHandler);
         mCameraCompatFreeformPolicy =
@@ -147,6 +147,7 @@
         cameraStateMonitor.startListeningToCameraState();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testFullscreen_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
@@ -157,6 +158,7 @@
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
@@ -164,12 +166,14 @@
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         assertNotInCameraCompatMode();
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -210,6 +214,7 @@
         assertActivityRefreshRequested(/* refreshRequested */ false);
     }
 
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -235,6 +240,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -246,6 +252,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
@@ -254,6 +261,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -268,6 +276,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
         when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
@@ -281,6 +290,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 85cb1bc..5c0d424 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -83,7 +83,7 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -2820,7 +2820,7 @@
         verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
     }
 
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() {
         doReturn(true).when(() ->
@@ -2829,7 +2829,7 @@
         assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
-    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @Test
     public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
         doReturn(true).when(() ->
@@ -2838,7 +2838,7 @@
         assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
-    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @Test
     public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 6c1e1a4..1294945 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -1027,28 +1027,36 @@
             boolean enabled = (mCurrentFunctions & UsbManager.FUNCTION_MIDI) != 0;
             if (enabled != mMidiEnabled) {
                 if (enabled) {
+                    boolean midiDeviceFound = false;
                     if (android.hardware.usb.flags.Flags.enableUsbSysfsMidiIdentification()) {
                         try {
                             getMidiCardDevice();
+                            midiDeviceFound = true;
                         } catch (FileNotFoundException e) {
-                            Slog.e(TAG, "could not identify MIDI device", e);
-                            enabled = false;
+                            Slog.w(TAG, "could not identify MIDI device", e);
                         }
-                    } else {
+                    }
+                    // For backward compatibility with older kernels without
+                    // https://lore.kernel.org/r/20240307030922.3573161-1-royluo@google.com
+                    if (!midiDeviceFound) {
                         Scanner scanner = null;
                         try {
                             scanner = new Scanner(new File(MIDI_ALSA_PATH));
                             mMidiCard = scanner.nextInt();
                             mMidiDevice = scanner.nextInt();
+                            midiDeviceFound = true;
                         } catch (FileNotFoundException e) {
                             Slog.e(TAG, "could not open MIDI file", e);
-                            enabled = false;
                         } finally {
                             if (scanner != null) {
                                 scanner.close();
                             }
                         }
                     }
+                    if (!midiDeviceFound) {
+                        Slog.e(TAG, "Failed to enable MIDI function");
+                        enabled = false;
+                    }
                 }
                 mMidiEnabled = enabled;
             }