Merge "[Expressive design] Add expandable preference" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 836d4e99..664dfe9 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4919,6 +4919,7 @@
     method public int getPendingIntentBackgroundActivityStartMode();
     method public int getPendingIntentCreatorBackgroundActivityStartMode();
     method public int getSplashScreenStyle();
+    method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public boolean isAllowPassThroughOnTouchOutside();
     method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed();
     method public boolean isShareIdentityEnabled();
     method public static android.app.ActivityOptions makeBasic();
@@ -4932,6 +4933,7 @@
     method public static android.app.ActivityOptions makeTaskLaunchBehind();
     method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
     method public void requestUsageTimeReport(android.app.PendingIntent);
+    method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public void setAllowPassThroughOnTouchOutside(boolean);
     method public android.app.ActivityOptions setAppVerificationBundle(android.os.Bundle);
     method public android.app.ActivityOptions setLaunchBounds(@Nullable android.graphics.Rect);
     method public android.app.ActivityOptions setLaunchDisplayId(int);
@@ -8775,6 +8777,11 @@
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
     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>);
+    method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
+    method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
+    field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0
+    field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2
+    field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1
   }
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
@@ -8816,6 +8823,7 @@
     field public static final String PROPERTY_RETURN_VALUE = "returnValue";
     field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
     field public static final int RESULT_DENIED = 1; // 0x1
+    field public static final int RESULT_DISABLED = 6; // 0x6
     field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
     field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
     field public static final int RESULT_OK = 0; // 0x0
@@ -46698,6 +46706,8 @@
     field public static final int TYPE_IMS = 64; // 0x40
     field public static final int TYPE_MCX = 1024; // 0x400
     field public static final int TYPE_MMS = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PAID = 65536; // 0x10000
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PRIVATE = 131072; // 0x20000
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int TYPE_RCS = 32768; // 0x8000
     field public static final int TYPE_SUPL = 4; // 0x4
     field public static final int TYPE_VSIM = 4096; // 0x1000
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a1561c2..bc34f5b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -15888,6 +15888,8 @@
     field public static final String TYPE_IMS_STRING = "ims";
     field public static final String TYPE_MCX_STRING = "mcx";
     field public static final String TYPE_MMS_STRING = "mms";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String TYPE_RCS_STRING = "rcs";
     field public static final String TYPE_SUPL_STRING = "supl";
     field public static final String TYPE_VSIM_STRING = "vsim";
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 92bca3c..9904632 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -21,14 +21,52 @@
         "**/*.aidl",
         ":framework-nfc-non-updatable-sources",
         ":messagequeue-gen",
+        ":ranging_stack_mock_initializer",
     ],
     // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
     exclude_srcs: [
         "android/os/*MessageQueue/**/*.java",
+        "android/ranging/**/*.java",
     ],
     visibility: ["//frameworks/base"],
 }
 
+//Mock to allow service registry for ranging stack.
+//TODO(b/331206299): Remove this after RELEASE_RANGING_STACK is ramped up to next.
+soong_config_module_type {
+    name: "ranging_stack_framework_mock_init",
+    module_type: "genrule",
+    config_namespace: "bootclasspath",
+    bool_variables: [
+        "release_ranging_stack",
+    ],
+    properties: [
+        "srcs",
+        "cmd",
+        "out",
+    ],
+}
+
+// The actual RangingFrameworkInitializer is present in packages/modules/Uwb/ranging/framework.
+// Mock RangingFrameworkInitializer does nothing and allows to successfully build
+// SystemServiceRegistry after registering for system service in SystemServiceRegistry both with
+// and without build flag RELEASE_RANGING_STACK enabled.
+ranging_stack_framework_mock_init {
+    name: "ranging_stack_mock_initializer",
+    soong_config_variables: {
+        release_ranging_stack: {
+            cmd: "touch $(out)",
+            // Adding an empty file as out is mandatory.
+            out: ["android/ranging/empty_ranging_fw.txt"],
+            conditions_default: {
+                srcs: ["android/ranging/mock/RangingFrameworkInitializer.java"],
+                cmd: "mkdir -p android/ranging/; cp $(in) $(out);",
+                out: ["android/ranging/RangingFrameworkInitializer.java"],
+            },
+        },
+    },
+}
+
 // Add selected MessageQueue.java implementation to srcs
 soong_config_module_type {
     name: "release_package_messagequeue_implementation_srcs",
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 91aa225..0d183c7 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -26,6 +26,7 @@
 import static android.view.Display.INVALID_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -453,6 +454,10 @@
     private static final String KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE =
             "android.activity.pendingIntentCreatorBackgroundActivityStartMode";
 
+    /** See {@link #setAllowPassThroughOnTouchOutside(boolean)}. */
+    private static final String KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE =
+            "android.activity.allowPassThroughOnTouchOutside";
+
     /**
      * @see #setLaunchCookie
      * @hide
@@ -554,6 +559,7 @@
     private int mPendingIntentCreatorBackgroundActivityStartMode =
             MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
     private boolean mDisableStartingWindow;
+    private boolean mAllowPassThroughOnTouchOutside;
 
     /**
      * Create an ActivityOptions specifying a custom animation to run when
@@ -1416,6 +1422,7 @@
                 KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE,
                 MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
         mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW);
+        mAllowPassThroughOnTouchOutside = opts.getBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE);
         mAnimationAbortListener = IRemoteCallback.Stub.asInterface(
                 opts.getBinder(KEY_ANIM_ABORT_LISTENER));
     }
@@ -1839,6 +1846,39 @@
                 && mLaunchIntoPipParams.isLaunchIntoPip();
     }
 
+    /**
+     * Returns whether the source activity allows the overlaying activities from the to-be-launched
+     * app to pass through touch events to it when touches fall outside the content window.
+     *
+     * @see #setAllowPassThroughOnTouchOutside(boolean)
+     */
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN)
+    public boolean isAllowPassThroughOnTouchOutside() {
+        return mAllowPassThroughOnTouchOutside;
+    }
+
+    /**
+     * Sets whether the source activity allows the overlaying activities from the to-be-launched
+     * app to pass through touch events to it when touches fall outside the content window.
+     *
+     * <p> By default, touches that fall on a translucent non-touchable area of an overlaying
+     * activity window are blocked from passing through to the activity below (source activity),
+     * unless the overlaying activity is from the same UID as the source activity. The source
+     * activity may use this method to opt in and allow the overlaying activities from the
+     * to-be-launched app to pass through touches to itself. The source activity needs to ensure
+     * that it trusts the overlaying activity and its content is not vulnerable to UI redressing
+     * attacks. The flag is ignored if the context calling
+     * {@link Context#startActivity(Intent, Bundle)} is not an activity.
+     *
+     * <p> For backward compatibility, apps with target SDK 35 and below may still receive
+     * pass-through touches without opt-in if the cross-uid activity is launched by the source
+     * activity.
+     */
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN)
+    public void setAllowPassThroughOnTouchOutside(boolean allowed) {
+        mAllowPassThroughOnTouchOutside = allowed;
+    }
+
     /** @hide */
     public int getLaunchActivityType() {
         return mLaunchActivityType;
@@ -2520,6 +2560,10 @@
         if (mDisableStartingWindow) {
             b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow);
         }
+        if (mAllowPassThroughOnTouchOutside) {
+            b.putBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE,
+                    mAllowPassThroughOnTouchOutside);
+        }
         b.putBinder(KEY_ANIM_ABORT_LISTENER,
                 mAnimationAbortListener != null ? mAnimationAbortListener.asBinder() : null);
         return b;
diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java
index b781ce5..f21c3e8 100644
--- a/core/java/android/app/DownloadManager.java
+++ b/core/java/android/app/DownloadManager.java
@@ -493,6 +493,9 @@
          * {@link Environment#getExternalStoragePublicDirectory(String)} with
          * {@link Environment#DIRECTORY_DOWNLOADS}).
          *
+         * All non-visible downloads that are not modified in the last 7 days will be deleted during
+         * idle runs.
+         *
          * @param uri a file {@link Uri} indicating the destination for the downloaded file.
          * @return this object
          */
@@ -796,7 +799,9 @@
          * public Downloads directory (as returned by
          * {@link Environment#getExternalStoragePublicDirectory(String)} with
          * {@link Environment#DIRECTORY_DOWNLOADS}) will be visible in system's Downloads UI
-         * and the rest will not be visible.
+         * and the rest will not be visible. All non-visible downloads that are not modified
+         * in the last 7 days will be deleted during idle runs.
+         *
          * (e.g. {@link Context#getExternalFilesDir(String)}) will not be visible.
          */
         @Deprecated
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index a39f216..c21fe0e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1619,22 +1619,6 @@
     public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
 
     /**
-     * {@link #extras} key: {@link Icon} of an image used as an overlay Icon on
-     * {@link Notification#mLargeIcon} for {@link EnRouteStyle} notifications.
-     * This extra is an {@code Icon}.
-     * @hide
-     */
-    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
-    public static final String EXTRA_ENROUTE_OVERLAY_ICON = "android.enrouteOverlayIcon";
-
-    /**
-     * {@link #extras} key: text used as a sub-text for the largeIcon of
-     * {@link EnRouteStyle} notification. This extra is a {@code CharSequence}.
-     * @hide
-     */
-    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
-    public static final String EXTRA_ENROUTE_LARGE_ICON_SUBTEXT = "android.enrouteLargeIconSubText";
-    /**
      * {@link #extras} key: whether the notification should be colorized as
      * supplied to {@link Builder#setColorized(boolean)}.
      */
@@ -3152,7 +3136,6 @@
         }
 
         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));
@@ -11173,145 +11156,6 @@
     }
 
     /**
-     * TODO(b/360827871): Make EnRouteStyle public.
-     * A style used to represent the progress of a real-world journey with a known destination.
-     * For example:
-     * <ul>
-     *     <li>Delivery tracking</li>
-     *     <li>Ride progress</li>
-     *     <li>Flight tracking</li>
-     * </ul>
-     *
-     * The exact fields from {@link Notification} that are shown with this style may vary by
-     * the surface where this update appears, but the following fields are recommended:
-     * <ul>
-     *     <li>{@link Notification.Builder#setContentTitle}</li>
-     *     <li>{@link Notification.Builder#setContentText}</li>
-     *     <li>{@link Notification.Builder#setSubText}</li>
-     *     <li>{@link Notification.Builder#setLargeIcon}</li>
-     *     <li>{@link Notification.Builder#setProgress}</li>
-     *     <li>{@link Notification.Builder#setWhen} - This should be the future time of the next,
-     *     final, or most important stop on this journey.</li>
-     * </ul>
-     * @hide
-     */
-    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
-    public static class EnRouteStyle extends Notification.Style {
-
-        @Nullable
-        private Icon mOverlayIcon = null;
-
-        @Nullable
-        private CharSequence mLargeIconSubText = null;
-
-        public EnRouteStyle() {
-        }
-
-        /**
-         * Returns the overlay icon to be displayed on {@link Notification#mLargeIcon}.
-         * @see EnRouteStyle#setOverlayIcon
-         */
-        @Nullable
-        public Icon getOverlayIcon() {
-            return mOverlayIcon;
-        }
-
-        /**
-         * Optional icon to be displayed on {@link Notification#mLargeIcon}.
-         *
-         * This image will be cropped to a circle and will obscure
-         * a semicircle of the right side of the large icon.
-         */
-        @NonNull
-        public EnRouteStyle setOverlayIcon(@Nullable Icon overlayIcon) {
-            mOverlayIcon = overlayIcon;
-            return this;
-        }
-
-        /**
-         * Returns the sub-text for {@link Notification#mLargeIcon}.
-         * @see EnRouteStyle#setLargeIconSubText
-         */
-        @Nullable
-        public CharSequence getLargeIconSubText() {
-            return mLargeIconSubText;
-        }
-
-        /**
-         * Optional text which generally related to
-         * the {@link Notification.Builder#setLargeIcon} or {@link #setOverlayIcon} or both.
-         */
-        @NonNull
-        public EnRouteStyle setLargeIconSubText(@Nullable CharSequence largeIconSubText) {
-            mLargeIconSubText = stripStyling(largeIconSubText);
-            return this;
-        }
-
-         /**
-         * @hide
-         */
-        @Override
-        public boolean areNotificationsVisiblyDifferent(Style other) {
-            if (other == null || getClass() != other.getClass()) {
-                return true;
-            }
-
-            final EnRouteStyle enRouteStyle = (EnRouteStyle) other;
-            return !Objects.equals(mOverlayIcon, enRouteStyle.mOverlayIcon)
-                    || !Objects.equals(mLargeIconSubText, enRouteStyle.mLargeIconSubText);
-        }
-
-        /**
-         * @hide
-         */
-        @Override
-        public void addExtras(Bundle extras) {
-            super.addExtras(extras);
-            extras.putParcelable(EXTRA_ENROUTE_OVERLAY_ICON, mOverlayIcon);
-            extras.putCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT, mLargeIconSubText);
-        }
-
-        /**
-         * @hide
-         */
-        @Override
-        protected void restoreFromExtras(Bundle extras) {
-            super.restoreFromExtras(extras);
-            mOverlayIcon = extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class);
-            mLargeIconSubText = extras.getCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT);
-        }
-
-        /**
-         * @hide
-         */
-        @Override
-        public void purgeResources() {
-            super.purgeResources();
-            if (mOverlayIcon != null) {
-                mOverlayIcon.convertToAshmem();
-            }
-        }
-
-        /**
-         * @hide
-         */
-        @Override
-        public void reduceImageSizes(Context context) {
-            super.reduceImageSizes(context);
-            if (mOverlayIcon != null) {
-                final Resources resources = context.getResources();
-                final boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
-
-                int rightIconSize = resources.getDimensionPixelSize(isLowRam
-                        ? R.dimen.notification_right_icon_size_low_ram
-                        : R.dimen.notification_right_icon_size);
-                mOverlayIcon.scaleDownIfNecessary(rightIconSize, rightIconSize);
-            }
-        }
-    }
-
-
-    /**
      * 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.
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c13a58f..ea4148c 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -230,6 +230,7 @@
 import android.print.PrintManager;
 import android.provider.E2eeContactKeysManager;
 import android.provider.ProviderFrameworkInitializer;
+import android.ranging.RangingFrameworkInitializer;
 import android.safetycenter.SafetyCenterFrameworkInitializer;
 import android.scheduling.SchedulingFrameworkInitializer;
 import android.security.FileIntegrityManager;
@@ -1825,6 +1826,12 @@
             if (android.webkit.Flags.updateServiceIpcWrapper()) {
                 WebViewBootstrapFrameworkInitializer.registerServiceWrappers();
             }
+            // This is guarded by aconfig flag "com.android.ranging.flags.ranging_stack_enabled"
+            // when the build flag RELEASE_RANGING_STACK is enabled. When disabled, this calls the
+            // mock RangingFrameworkInitializer#registerServiceWrappers which is no-op. As the
+            // aconfig lib for ranging module is built only if  RELEASE_RANGING_STACK is enabled,
+            // flagcannot be added here.
+            RangingFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 38f59ad..c1c96ea 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -299,6 +299,14 @@
             "android.service.wallpaper.extra.FROM_FOREGROUND_APP";
 
     /**
+     * Extra passed on {@link Intent.ACTION_WALLPAPER_CHANGED} indicating if wallpaper was set from
+     * a foreground app.
+     * @hide
+     */
+    public static final String EXTRA_WHICH_WALLPAPER_CHANGED =
+            "android.service.wallpaper.extra.WHICH_WALLPAPER_CHANGED";
+
+    /**
      * The different screen orientations. {@link #getOrientation} provides their exact definition.
      * This is only used internally by the framework and the WallpaperBackupAgent.
      * @hide
diff --git a/core/java/android/app/admin/PolicySizeVerifier.java b/core/java/android/app/admin/PolicySizeVerifier.java
index 7f8e50e..1e03e1f 100644
--- a/core/java/android/app/admin/PolicySizeVerifier.java
+++ b/core/java/android/app/admin/PolicySizeVerifier.java
@@ -22,7 +22,9 @@
 import android.os.PersistableBundle;
 
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.ModifiedUtf8;
 
+import java.io.UTFDataFormatException;
 import java.util.ArrayDeque;
 import java.util.Queue;
 
@@ -33,8 +35,6 @@
  */
 public class PolicySizeVerifier {
 
-    // Binary XML serializer doesn't support longer strings
-    public static final int MAX_POLICY_STRING_LENGTH = 65535;
     // FrameworkParsingPackageUtils#MAX_FILE_NAME_SIZE, Android packages are used in dir names.
     public static final int MAX_PACKAGE_NAME_LENGTH = 223;
 
@@ -47,8 +47,11 @@
      * Throw if string argument is too long to be serialized.
      */
     public static void enforceMaxStringLength(String str, String argName) {
-        Preconditions.checkArgument(
-                str.length() <= MAX_POLICY_STRING_LENGTH, argName + " loo long");
+        try {
+            long len = ModifiedUtf8.countBytes(str, /* throw error if too long */ true);
+        } catch (UTFDataFormatException e) {
+            throw new IllegalArgumentException(argName + " too long");
+        }
     }
 
     /**
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 216ba5d..6797a51 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -22,15 +22,21 @@
 import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
+import android.app.appsearch.AppSearchManager;
 import android.content.Context;
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.ParcelableException;
 import android.os.RemoteException;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -45,10 +51,44 @@
 @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
 @SystemService(Context.APP_FUNCTION_SERVICE)
 public final class AppFunctionManager {
+
+    /**
+     * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset
+     * enabled state to the default value.
+     */
+    public static final int APP_FUNCTION_STATE_DEFAULT = 0;
+
+    /**
+     * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled}
+     * with this value.
+     */
+    public static final int APP_FUNCTION_STATE_ENABLED = 1;
+
+    /**
+     * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled}
+     * with this value.
+     */
+    public static final int APP_FUNCTION_STATE_DISABLED = 2;
+
     private final IAppFunctionManager mService;
     private final Context mContext;
 
     /**
+     * The enabled state of the app function.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"APP_FUNCTION_STATE_"},
+            value = {
+                APP_FUNCTION_STATE_DEFAULT,
+                APP_FUNCTION_STATE_ENABLED,
+                APP_FUNCTION_STATE_DISABLED
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EnabledState {}
+
+    /**
      * Creates an instance.
      *
      * @param service An interface to the backing service.
@@ -164,4 +204,118 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Returns a boolean through a callback, indicating whether the app function is enabled.
+     *
+     * <p>* This method can only check app functions owned by the caller, or those where the caller
+     * has visibility to the owner package and holds either the {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission.
+     *
+     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+     *
+     * <ul>
+     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+     *       have access to it.
+     * </ul>
+     *
+     * @param functionIdentifier the identifier of the app function to check (unique within the
+     *     target package) and in most cases, these are automatically generated by the AppFunctions
+     *     SDK
+     * @param targetPackage the package name of the app function's owner
+     * @param executor the executor to run the request
+     * @param callback the callback to receive the function enabled check result
+     */
+    public void isAppFunctionEnabled(
+            @NonNull String functionIdentifier,
+            @NonNull String targetPackage,
+            @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Boolean, Exception> callback) {
+        Objects.requireNonNull(functionIdentifier);
+        Objects.requireNonNull(targetPackage);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        AppSearchManager appSearchManager = mContext.getSystemService(AppSearchManager.class);
+        if (appSearchManager == null) {
+            callback.onError(new IllegalStateException("Failed to get AppSearchManager."));
+            return;
+        }
+
+        AppFunctionManagerHelper.isAppFunctionEnabled(
+                functionIdentifier, targetPackage, appSearchManager, executor, callback);
+    }
+
+    /**
+     * Sets the enabled state of the app function owned by the calling package.
+     *
+     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+     *
+     * <ul>
+     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+     *       have access to it.
+     * </ul>
+     *
+     * @param functionIdentifier the identifier of the app function to enable (unique within the
+     *     calling package). In most cases, identifiers are automatically generated by the
+     *     AppFunctions SDK
+     * @param newEnabledState the new state of the app function
+     * @param executor the executor to run the callback
+     * @param callback the callback to receive the result of the function enablement. The call was
+     *     successful if no exception was thrown.
+     */
+    @UserHandleAware
+    public void setAppFunctionEnabled(
+            @NonNull String functionIdentifier,
+            @EnabledState int newEnabledState,
+            @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Void, Exception> callback) {
+        Objects.requireNonNull(functionIdentifier);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback);
+        try {
+            mService.setAppFunctionEnabled(
+                    mContext.getPackageName(),
+                    functionIdentifier,
+                    mContext.getUser(),
+                    newEnabledState,
+                    callbackWrapper);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub {
+
+        private final OutcomeReceiver<Void, Exception> mCallback;
+        private final Executor mExecutor;
+
+        CallbackWrapper(
+                @NonNull Executor callbackExecutor,
+                @NonNull OutcomeReceiver<Void, Exception> callback) {
+            mCallback = callback;
+            mExecutor = callbackExecutor;
+        }
+
+        @Override
+        public void onSuccess() {
+            mExecutor.execute(() -> mCallback.onResult(null));
+        }
+
+        @Override
+        public void onError(@NonNull ParcelableException exception) {
+            mExecutor.execute(() -> {
+                if (IllegalArgumentException.class.isAssignableFrom(
+                        exception.getCause().getClass())) {
+                    mCallback.onError((IllegalArgumentException) exception.getCause());
+                } else if (SecurityException.class.isAssignableFrom(
+                        exception.getCause().getClass())) {
+                    mCallback.onError((SecurityException) exception.getCause());
+                } else {
+                    mCallback.onError(exception);
+                }
+            });
+        }
+    }
 }
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index d6f45e4..fe2db49 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -22,7 +22,7 @@
 import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT;
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 
-import android.annotation.CallbackExecutor;
+import android.Manifest;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchManager;
@@ -33,6 +33,7 @@
 import android.app.appsearch.SearchResults;
 import android.app.appsearch.SearchSpec;
 import android.os.OutcomeReceiver;
+import android.text.TextUtils;
 
 import java.io.IOException;
 import java.util.List;
@@ -50,73 +51,69 @@
     /**
      * Returns (through a callback) a boolean indicating whether the app function is enabled.
      *
-     * <p>This method can only check app functions that are owned by the caller owned by packages
-     * visible to the caller.
+     * This method can only check app functions owned by the caller, or those where the caller
+     * has visibility to the owner package and holds either the {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission.
      *
      * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
      *
      * <ul>
-     *   <li>{@link IllegalArgumentException}, if the function is not found
-     *   <li>{@link SecurityException}, if the caller does not have permission to query the target
-     *       package
+     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+     *       have access to it.
      * </ul>
      *
      * @param functionIdentifier the identifier of the app function to check (unique within the
-     *     target package) and in most cases, these are automatically generated by the AppFunctions
-     *     SDK
-     * @param targetPackage the package name of the app function's owner
-     * @param appSearchExecutor the executor to run the metadata search mechanism through AppSearch
-     * @param callbackExecutor the executor to run the callback
-     * @param callback the callback to receive the function enabled check result
+     *                           target package) and in most cases, these are automatically
+     *                           generated by the AppFunctions
+     *                           SDK
+     * @param targetPackage      the package name of the app function's owner
+     * @param executor           executor the executor to run the request
+     * @param callback           the callback to receive the function enabled check result
      * @hide
      */
     public static void isAppFunctionEnabled(
             @NonNull String functionIdentifier,
             @NonNull String targetPackage,
             @NonNull AppSearchManager appSearchManager,
-            @NonNull Executor appSearchExecutor,
-            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull Executor executor,
             @NonNull OutcomeReceiver<Boolean, Exception> callback) {
         Objects.requireNonNull(functionIdentifier);
         Objects.requireNonNull(targetPackage);
         Objects.requireNonNull(appSearchManager);
-        Objects.requireNonNull(appSearchExecutor);
-        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
         appSearchManager.createGlobalSearchSession(
-                appSearchExecutor,
+                executor,
                 (searchSessionResult) -> {
                     if (!searchSessionResult.isSuccess()) {
-                        callbackExecutor.execute(
-                                () ->
-                                        callback.onError(
-                                                failedResultToException(searchSessionResult)));
+                        callback.onError(failedResultToException(searchSessionResult));
                         return;
                     }
                     try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) {
                         SearchResults results =
                                 searchJoinedStaticWithRuntimeAppFunctions(
-                                        searchSession, targetPackage, functionIdentifier);
+                                        Objects.requireNonNull(searchSession),
+                                        targetPackage,
+                                        functionIdentifier);
                         results.getNextPage(
-                                appSearchExecutor,
-                                listAppSearchResult ->
-                                        callbackExecutor.execute(
-                                                () -> {
-                                                    if (listAppSearchResult.isSuccess()) {
-                                                        callback.onResult(
-                                                                getEnabledStateFromSearchResults(
-                                                                        Objects.requireNonNull(
-                                                                                listAppSearchResult
+                                executor,
+                                listAppSearchResult -> {
+                                    if (listAppSearchResult.isSuccess()) {
+                                        callback.onResult(
+                                                getEffectiveEnabledStateFromSearchResults(
+                                                        Objects.requireNonNull(
+                                                                listAppSearchResult
                                                                         .getResultValue())));
-                                                    } else {
-                                                        callback.onError(
-                                                                failedResultToException(
-                                                                        listAppSearchResult));
-                                                    }
-                                                }));
+                                    } else {
+                                        callback.onError(
+                                                failedResultToException(listAppSearchResult));
+                                    }
+                                });
+                        results.close();
                     } catch (Exception e) {
-                        callbackExecutor.execute(() -> callback.onError(e));
+                        callback.onError(e);
                     }
                 });
     }
@@ -124,56 +121,58 @@
     /**
      * Searches joined app function static and runtime metadata using the function Id and the
      * package.
-     *
-     * @hide
      */
     private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions(
             @NonNull GlobalSearchSession session,
             @NonNull String targetPackage,
             @NonNull String functionIdentifier) {
         SearchSpec runtimeSearchSpec =
-                getAppFunctionRuntimeMetadataSearchSpecByFunctionId(targetPackage);
+                getAppFunctionRuntimeMetadataSearchSpecByPackageName(targetPackage);
         JoinSpec joinSpec =
                 new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID)
-                        .setNestedSearch(functionIdentifier, runtimeSearchSpec)
+                        .setNestedSearch(
+                                buildFilerRuntimeMetadataByFunctionIdQuery(functionIdentifier),
+                                runtimeSearchSpec)
                         .build();
         SearchSpec joinedStaticWithRuntimeSearchSpec =
                 new SearchSpec.Builder()
-                        .setJoinSpec(joinSpec)
                         .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                         .addFilterSchemas(
                                 AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage(
                                         targetPackage))
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setJoinSpec(joinSpec)
+                        .setVerbatimSearchEnabled(true)
                         .build();
-        return session.search(functionIdentifier, joinedStaticWithRuntimeSearchSpec);
+        return session.search(
+                buildFilerStaticMetadataByFunctionIdQuery(functionIdentifier),
+                joinedStaticWithRuntimeSearchSpec);
     }
 
     /**
-     * Finds whether the function is enabled or not from the search results returned by {@link
-     * #searchJoinedStaticWithRuntimeAppFunctions}.
+     * Returns whether the function is effectively enabled or not from the search results returned
+     * by {@link #searchJoinedStaticWithRuntimeAppFunctions}.
      *
+     * @param joinedStaticRuntimeResults search results joining AppFunctionStaticMetadata
+     *                                   and AppFunctionRuntimeMetadata.
      * @throws IllegalArgumentException if the function is not found in the results
-     * @hide
      */
-    private static boolean getEnabledStateFromSearchResults(
+    private static boolean getEffectiveEnabledStateFromSearchResults(
             @NonNull List<SearchResult> joinedStaticRuntimeResults) {
         if (joinedStaticRuntimeResults.isEmpty()) {
-            // Function not found.
             throw new IllegalArgumentException("App function not found.");
         } else {
             List<SearchResult> runtimeMetadataResults =
                     joinedStaticRuntimeResults.getFirst().getJoinedResults();
-            if (!runtimeMetadataResults.isEmpty()) {
-                Boolean result =
-                        (Boolean)
-                                runtimeMetadataResults
-                                        .getFirst()
-                                        .getGenericDocument()
-                                        .getProperty(PROPERTY_ENABLED);
-                if (result != null) {
-                    return result;
-                }
+            if (runtimeMetadataResults.isEmpty()) {
+                throw new IllegalArgumentException("App function not found.");
+            }
+            boolean[] enabled =
+                    runtimeMetadataResults
+                            .getFirst()
+                            .getGenericDocument()
+                            .getPropertyBooleanArray(PROPERTY_ENABLED);
+            if (enabled != null && enabled.length != 0) {
+                return enabled[0];
             }
             // Runtime metadata not found. Using the default value in the static metadata.
             return joinedStaticRuntimeResults
@@ -186,36 +185,39 @@
     /**
      * Returns search spec that queries app function metadata for a specific package name by its
      * function identifier.
-     *
-     * @hide
      */
-    public static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByFunctionId(
+    private static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByPackageName(
             @NonNull String targetPackage) {
         return new SearchSpec.Builder()
                 .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                 .addFilterSchemas(
                         AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage))
-                .addFilterProperties(
-                        AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage),
-                        List.of(AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID))
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setVerbatimSearchEnabled(true)
                 .build();
     }
 
-    /**
-     * Converts a failed app search result codes into an exception.
-     *
-     * @hide
-     */
-    public static @NonNull Exception failedResultToException(
+    private static String buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier) {
+        return TextUtils.formatSimple("%s:\"%s\"",
+                AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
+                functionIdentifier);
+    }
+
+    private static String buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier) {
+        return TextUtils.formatSimple("%s:\"%s\"",
+                AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
+                functionIdentifier);
+    }
+
+    /** Converts a failed app search result codes into an exception. */
+    private static @NonNull Exception failedResultToException(
             @NonNull AppSearchResult appSearchResult) {
         return switch (appSearchResult.getResultCode()) {
-            case AppSearchResult.RESULT_INVALID_ARGUMENT ->
-                    new IllegalArgumentException(appSearchResult.getErrorMessage());
-            case AppSearchResult.RESULT_IO_ERROR ->
-                    new IOException(appSearchResult.getErrorMessage());
-            case AppSearchResult.RESULT_SECURITY_ERROR ->
-                    new SecurityException(appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
+                    appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_IO_ERROR -> new IOException(
+                    appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
+                    appSearchResult.getErrorMessage());
             default -> new IllegalStateException(appSearchResult.getErrorMessage());
         };
     }
diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
index 83b5aa0..8b7f326 100644
--- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
+++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
@@ -204,11 +204,17 @@
                             packageName, functionId));
         }
 
+        public Builder(AppFunctionRuntimeMetadata original) {
+            this(original.getPackageName(), original.getFunctionId());
+            setEnabled(original.getEnabled());
+        }
+
         /**
          * Sets an indicator specifying if the function is enabled or not. This would override the
          * default enabled state in the static metadata ({@link
-         * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to
-         * null to clear the override.
+         * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to null
+         * to clear the override.
+         * TODO(369683073) Replace the tristate Boolean with IntDef EnabledState.
          */
         @NonNull
         public Builder setEnabled(@Nullable Boolean enabled) {
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index f6580e6..4ed0a1b 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -99,6 +99,9 @@
     /** The operation was timed out. */
     public static final int RESULT_TIMED_OUT = 5;
 
+    /** The caller tried to execute a disabled app function. */
+    public static final int RESULT_DISABLED = 6;
+
     /** The result code of the app function execution. */
     @ResultCode private final int mResultCode;
 
@@ -274,6 +277,7 @@
                 RESULT_INTERNAL_ERROR,
                 RESULT_INVALID_ARGUMENT,
                 RESULT_TIMED_OUT,
+                RESULT_DISABLED,
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ResultCode {}
diff --git a/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl
new file mode 100644
index 0000000..ced4155
--- /dev/null
+++ b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package android.app.appfunctions;
+
+import android.os.ParcelableException;
+
+/**
+ * @hide
+ */
+oneway interface IAppFunctionEnabledCallback {
+    void onSuccess();
+    void onError(in ParcelableException exception);
+}
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index c63217f..72335e4 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -17,9 +17,11 @@
 package android.app.appfunctions;
 
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.IAppFunctionEnabledCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.os.ICancellationSignal;
 
+import android.os.UserHandle;
 /**
  * Defines the interface for apps to interact with the app function execution service
  * {@code AppFunctionManagerService} running in the system server process.
@@ -37,4 +39,15 @@
         in ExecuteAppFunctionAidlRequest request,
         in IExecuteAppFunctionCallback callback
     );
+
+    /**
+    * Sets an AppFunction's enabled state provided by {@link AppFunctionService} through the system.
+    */
+    void setAppFunctionEnabled(
+        in String callingPackage,
+        in String functionIdentifier,
+        in UserHandle userHandle,
+        int enabledState,
+        in IAppFunctionEnabledCallback callback
+    );
 }
diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig
index 4091622..c5bd56f 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -1,8 +1,16 @@
 package: "android.app"
 container: "system"
+
 flag {
   name: "remove_next_wallpaper_component"
   namespace: "systemui"
   description: "Remove deprecated field WallpaperData#nextWallpaperComponent. Only effective after rebooting."
   bug: "365991991"
 }
+
+flag {
+  name: "fix_wallpaper_changed"
+  namespace: "systemui"
+  description: "Fixes timing of wallpaper changed notification and adds extra information. Only effective after rebooting."
+  bug: "369814294"
+}
diff --git a/core/java/android/ranging/mock/RangingFrameworkInitializer.java b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
new file mode 100644
index 0000000..540f519
--- /dev/null
+++ b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ranging;
+
+/**
+* Mock RangingFrameworkInitializer.
+*
+* @hide
+*/
+
+// TODO(b/331206299): Remove this after RANGING_STACK_ENABLED is ramped up to next.
+public final class RangingFrameworkInitializer {
+    private RangingFrameworkInitializer() {}
+    /**
+     * @hide
+     */
+    public static void registerServiceWrappers() {
+        // No-op.
+    }
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 303197d..e173255 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1079,9 +1079,12 @@
                         // in ensureManualZenRule() and setManualZenMode().
                         rt.manualRule.pkg = PACKAGE_ANDROID;
                         rt.manualRule.type = AutomaticZenRule.TYPE_OTHER;
-                        rt.manualRule.condition = new Condition(
-                                rt.manualRule.conditionId != null ? rt.manualRule.conditionId
-                                        : Uri.EMPTY, "", Condition.STATE_TRUE);
+                        // conditionId in rule must match condition.id to pass isValidManualRule().
+                        if (rt.manualRule.conditionId == null) {
+                            rt.manualRule.conditionId = Uri.EMPTY;
+                        }
+                        rt.manualRule.condition = new Condition(rt.manualRule.conditionId, "",
+                                Condition.STATE_TRUE);
                     }
                 }
                 return rt;
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 31a8dfa..1ea58bc 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -48,6 +48,7 @@
 per-file VelocityTracker.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file VerifiedInputEvent.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file VerifiedInputEvent.aidl = file:/services/core/java/com/android/server/input/OWNERS
+per-file LetterboxScrollProcessor*.java = file:/services/core/java/com/android/server/input/OWNERS
 
 # InputWindowHandle
 per-file InputWindowHandle.java  = file:/services/core/java/com/android/server/input/OWNERS
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 66776ce..ac208b5 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2064,7 +2064,11 @@
         if (mAttachInfo.mThreadedRenderer == null) return;
         if (mAttachInfo.mThreadedRenderer.setForceDark(determineForceDarkType())) {
             // TODO: Don't require regenerating all display lists to apply this setting
-            invalidateWorld(mView);
+            if (forceInvertColor()) {
+                destroyAndInvalidate();
+            } else {
+                invalidateWorld(mView);
+            }
         }
     }
 
@@ -11911,15 +11915,23 @@
         public void onHighTextContrastStateChanged(boolean enabled) {
             ThreadedRenderer.setHighContrastText(enabled);
 
-            // Destroy Displaylists so they can be recreated with high contrast recordings
-            destroyHardwareResources();
-
-            // Schedule redraw, which will rerecord + redraw all text
-            invalidate();
+            destroyAndInvalidate();
         }
     }
 
     /**
+     * Destroy Displaylists so they can be recreated with new recordings, in case you are changing
+     * the way things are rendered (e.g. high contrast, force dark), then invalidate to trigger a
+     * redraw.
+     */
+    private void destroyAndInvalidate() {
+        destroyHardwareResources();
+
+        // Schedule redraw, which will rerecord + redraw all text
+        invalidate();
+    }
+
+    /**
      * This class is an interface this ViewAncestor provides to the
      * AccessibilityManagerService to the latter can interact with
      * the view hierarchy in this ViewAncestor.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 2f649c2..1e5c6d8 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -465,13 +465,6 @@
     private static final long USE_ASYNC_SHOW_HIDE_METHOD = 352594277L; // This is a bug id.
 
     /**
-     * Version-gating is guarded by bug-fix flag.
-     */
-    private static final boolean ASYNC_SHOW_HIDE_METHOD_ENABLED =
-            !Flags.compatchangeForZerojankproxy()
-                || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
-
-    /**
      * If {@code true}, avoid calling the
      * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService}
      * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus}
@@ -614,6 +607,15 @@
     @UnsupportedAppUsage
     Rect mCursorRect = new Rect();
 
+    /**
+     * Version-gating is guarded by bug-fix flag.
+     */
+    // Note: this is non-static so that it only gets initialized once CompatChanges has
+    // access to the correct application context.
+    private final boolean mAsyncShowHideMethodEnabled =
+            !Flags.compatchangeForZerojankproxy()
+                    || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
+
     /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */
     @GuardedBy("mH")
     private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache;
@@ -2419,7 +2421,7 @@
                         mCurRootView.getLastClickToolType(),
                         resultReceiver,
                         reason,
-                        ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2463,7 +2465,7 @@
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
                     reason,
-                    ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -2572,7 +2574,7 @@
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
-                        statsToken, flags, resultReceiver, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        statsToken, flags, resultReceiver, reason, mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2615,7 +2617,7 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(),
-                    statsToken, flags, null, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    statsToken, flags, null, reason, mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -3392,7 +3394,7 @@
                         servedInputConnection == null ? null
                                 : servedInputConnection.asIRemoteAccessibilityInputConnection(),
                         view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
-                        mImeDispatcher, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mImeDispatcher, mAsyncShowHideMethodEnabled);
             } else {
                 res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                         startInputReason, mClient, windowGainingFocus, startInputFlags,
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index b0e38e2..cff42fb 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -399,13 +399,10 @@
                 return -1;
             }
             case "enable-text" -> {
-                if (mViewerConfigReader != null) {
-                    mViewerConfigReader.loadViewerConfig(groups, logger);
-                }
-                return setTextLogging(true, logger, groups);
+                return startLoggingToLogcat(groups, logger);
             }
             case "disable-text" -> {
-                return setTextLogging(false, logger, groups);
+                return stopLoggingToLogcat(groups, logger);
             }
             default -> {
                 return unknownCommand(pw);
diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java
index 1e6ba30..00ef80a 100644
--- a/core/java/com/android/internal/protolog/Utils.java
+++ b/core/java/com/android/internal/protolog/Utils.java
@@ -93,8 +93,7 @@
                     os.write(TAG, tag);
                     break;
                 default:
-                    throw new RuntimeException(
-                            "Unexpected field id " + pis.getFieldNumber());
+                    Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber());
             }
         }
 
@@ -126,8 +125,7 @@
                     os.write(LOCATION, pis.readString(LOCATION));
                     break;
                 default:
-                    throw new RuntimeException(
-                            "Unexpected field id " + pis.getFieldNumber());
+                    Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber());
             }
         }
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 92c3906..5c0dca2 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6183,6 +6183,10 @@
         is enabled and activity is connected to the camera in fullscreen. -->
     <bool name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled">false</bool>
 
+    <!-- Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+        for treatment is connected to the camera. -->
+    <item name="config_windowManagerCameraCompatAspectRatio" format="float" type="dimen">1.0</item>
+
     <!-- Docking is a uiMode configuration change and will cause activities to relaunch if it's not
          handled. If true, the configuration change will be sent but activities will not be
          relaunched upon docking. Apps with desk resources will behave like normal, since they may
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5f40a6c..807df1b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4778,6 +4778,7 @@
   <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" />
+  <java-symbol type="dimen" name="config_windowManagerCameraCompatAspectRatio" />
   <java-symbol type="bool" name="config_skipActivityRelaunchWhenDocking" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
diff --git a/graphics/java/android/graphics/PathIterator.java b/graphics/java/android/graphics/PathIterator.java
index 48b29f4..d7caabf 100644
--- a/graphics/java/android/graphics/PathIterator.java
+++ b/graphics/java/android/graphics/PathIterator.java
@@ -44,6 +44,8 @@
     private final Path mPath;
     private final int mPathGenerationId;
     private static final int POINT_ARRAY_SIZE = 8;
+    private static final boolean IS_DALVIK = "dalvik".equalsIgnoreCase(
+            System.getProperty("java.vm.name"));
 
     private static final NativeAllocationRegistry sRegistry =
             NativeAllocationRegistry.createMalloced(
@@ -80,9 +82,14 @@
         mPath = path;
         mNativeIterator = nCreate(mPath.mNativePath);
         mPathGenerationId = mPath.getGenerationId();
-        final VMRuntime runtime = VMRuntime.getRuntime();
-        mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
-        mPointsAddress = runtime.addressOf(mPointsArray);
+        if (IS_DALVIK) {
+            final VMRuntime runtime = VMRuntime.getRuntime();
+            mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
+            mPointsAddress = runtime.addressOf(mPointsArray);
+        } else {
+            mPointsArray = new float[POINT_ARRAY_SIZE];
+            mPointsAddress = 0;
+        }
         sRegistry.registerNativeAllocation(this, mNativeIterator);
     }
 
@@ -177,7 +184,8 @@
             throw new ConcurrentModificationException(
                     "Iterator cannot be used on modified Path");
         }
-        @Verb int verb = nNext(mNativeIterator, mPointsAddress);
+        @Verb int verb = IS_DALVIK
+            ? nNext(mNativeIterator, mPointsAddress) : nNextHost(mNativeIterator, mPointsArray);
         if (verb == VERB_DONE) {
             mDone = true;
         }
@@ -287,6 +295,9 @@
     private static native long nCreate(long nativePath);
     private static native long nGetFinalizer();
 
+    /* nNextHost should be used for host runtimes, e.g. LayoutLib */
+    private static native int nNextHost(long nativeIterator, float[] points);
+
     // ------------------ Critical JNI ------------------------
 
     @CriticalNative
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
index bfccb29..e3a1d8a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
@@ -142,6 +142,19 @@
         }
     }
 
+    void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) {
+        // Clean-up the legacy states in the system
+        for (int i = mTaskFragmentInfos.size() - 1; i >= 0; i--) {
+            final TaskFragmentInfo info = mTaskFragmentInfos.valueAt(i);
+            mPresenter.deleteTaskFragment(wct, info.getFragmentToken());
+        }
+        mPresenter.setSavedState(new Bundle());
+
+        mParcelableTaskContainerDataList.clear();
+        mTaskFragmentInfos.clear();
+        mTaskFragmentParentInfos.clear();
+    }
+
     boolean hasPendingStateToRestore() {
         return !mParcelableTaskContainerDataList.isEmpty();
     }
@@ -196,6 +209,7 @@
 
             mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(),
                     mTaskFragmentParentInfos.get(taskContainer.getTaskId()));
+            mTaskFragmentParentInfos.remove(taskContainer.getTaskId());
             restoredAny = true;
         }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index db4bb0e..8345b40 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -56,6 +56,7 @@
 import android.annotation.CallbackExecutor;
 import android.app.Activity;
 import android.app.ActivityClient;
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityThread;
 import android.app.AppGlobals;
@@ -280,7 +281,7 @@
             mSplitRules.clear();
             mSplitRules.addAll(rules);
 
-            if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) {
+            if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) {
                 return;
             }
 
@@ -2893,6 +2894,36 @@
                 return;
             }
             synchronized (mLock) {
+                if (mPresenter.isWaitingToRebuildTaskContainers()) {
+                    Log.w(TAG, "Rebuilding aborted, clean up and restart");
+
+                    // Retrieve the Task intent.
+                    final int taskId = getTaskId(activity);
+                    Intent taskIntent = null;
+                    final ActivityManager am = activity.getSystemService(ActivityManager.class);
+                    final List<ActivityManager.AppTask> appTasks = am.getAppTasks();
+                    for (ActivityManager.AppTask appTask : appTasks) {
+                        if (appTask.getTaskInfo().taskId == taskId) {
+                            taskIntent = appTask.getTaskInfo().baseIntent.cloneFilter();
+                            break;
+                        }
+                    }
+
+                    // Clean up and abort the restoration
+                    // TODO(b/369488857): also to remove the non-organized activities in the Task?
+                    final TransactionRecord transactionRecord =
+                            mTransactionManager.startNewTransaction();
+                    final WindowContainerTransaction wct = transactionRecord.getTransaction();
+                    mPresenter.abortTaskContainerRebuilding(wct);
+                    transactionRecord.apply(false /* shouldApplyIndependently */);
+
+                    // Start the Task root activity.
+                    if (taskIntent != null) {
+                        activity.startActivity(taskIntent);
+                    }
+                    return;
+                }
+
                 final IBinder activityToken = activity.getActivityToken();
                 final IBinder initialTaskFragmentToken =
                         getTaskFragmentTokenFromActivityClientRecord(activity);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 0c0ded9..b498ee2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -187,10 +187,14 @@
         mBackupHelper.scheduleBackup();
     }
 
-    boolean isRebuildTaskContainersNeeded() {
+    boolean isWaitingToRebuildTaskContainers() {
         return mBackupHelper.hasPendingStateToRestore();
     }
 
+    void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) {
+        mBackupHelper.abortTaskContainerRebuilding(wct);
+    }
+
     boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
             @NonNull Set<EmbeddingRule> rules) {
         return mBackupHelper.rebuildTaskContainers(wct, rules);
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 dcc2d93..b453f1d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -156,7 +156,7 @@
         mSplitController = splitController;
         for (ParcelableTaskFragmentContainerData tfData :
                 data.getParcelableTaskFragmentContainerDataList()) {
-            final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken);
+            final TaskFragmentInfo info = taskFragmentInfoMap.remove(tfData.mToken);
             if (info != null && !info.isEmpty()) {
                 final TaskFragmentContainer container =
                         new TaskFragmentContainer(tfData, splitController, this);
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 63a2880..cf0a975 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -156,6 +156,13 @@
 }
 
 flag {
+    name: "enable_flexible_two_app_split"
+    namespace: "multitasking"
+    description: "Enables only 2 app 90:10 split"
+    bug: "349828130"
+}
+
+flag {
     name: "enable_flexible_split"
     namespace: "multitasking"
     description: "Enables flexibile split feature for split screen"
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
new file mode 100644
index 0000000..bfb6d4a
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
@@ -0,0 +1,4 @@
+jeremysim@google.com
+winsonc@google.com
+peanutbutter@google.com
+shuminghao@google.com
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 498dc8b..7f1e4a8 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -66,14 +66,54 @@
     public @interface SplitPosition {
     }
 
-    /** A snap target in the first half of the screen, where the split is roughly 30-70. */
-    public static final int SNAP_TO_30_70 = 0;
+    /**
+     * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_33_66 = 0;
 
-    /** The 50-50 snap target */
-    public static final int SNAP_TO_50_50 = 1;
+    /** A snap target for two apps, where the split is 50-50.  */
+    public static final int SNAP_TO_2_50_50 = 1;
 
-    /** A snap target in the latter half of the screen, where the split is roughly 70-30. */
-    public static final int SNAP_TO_70_30 = 2;
+    /**
+     * A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_66_33 = 2;
+
+    /**
+     * A snap target for two apps, where the split is 90-10. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_90_10 = 3;
+
+    /**
+     * A snap target for two apps, where the split is 10-90. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_10_90 = 4;
+
+    /**
+     * A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_3_33_33_33 = 5;
+
+    /**
+     * A snap target for three apps, where the split is 45-45-10. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_45_45_10 = 6;
+
+    /**
+     * A snap target for three apps, where the split is 10-45-45. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_10_45_45 = 7;
 
     /**
      * These snap targets are used for split pairs in a stable, non-transient state. They may be
@@ -81,9 +121,14 @@
      * {@link SnapPosition}.
      */
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
     })
     public @interface PersistentSnapPosition {}
 
@@ -91,9 +136,14 @@
      * Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
      */
     public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) {
-        return snapPosition == SNAP_TO_30_70
-                || snapPosition == SNAP_TO_50_50
-                || snapPosition == SNAP_TO_70_30;
+        return snapPosition == SNAP_TO_2_33_66
+                || snapPosition == SNAP_TO_2_50_50
+                || snapPosition == SNAP_TO_2_66_33
+                || snapPosition == SNAP_TO_2_90_10
+                || snapPosition == SNAP_TO_2_10_90
+                || snapPosition == SNAP_TO_3_33_33_33
+                || snapPosition == SNAP_TO_3_45_45_10
+                || snapPosition == SNAP_TO_3_10_45_45;
     }
 
     /** The divider doesn't snap to any target and is freely placeable. */
@@ -109,9 +159,14 @@
     public static final int SNAP_TO_MINIMIZE = 13;
 
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30,
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
             SNAP_TO_NONE,
             SNAP_TO_START_AND_DISMISS,
             SNAP_TO_END_AND_DISMISS,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index f7f45ae..9f100fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -19,9 +19,9 @@
 import static android.view.WindowManager.DOCKED_LEFT;
 import static android.view.WindowManager.DOCKED_RIGHT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
@@ -283,10 +283,10 @@
 
     private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
             int bottomPosition, int dividerMax) {
-        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_30_70);
+        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66);
         addMiddleTarget(isHorizontalDivision);
         maybeAddTarget(bottomPosition,
-                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_70_30);
+                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33);
     }
 
     private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
@@ -332,7 +332,7 @@
     private void addMiddleTarget(boolean isHorizontalDivision) {
         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
-        mTargets.add(new SnapTarget(position, SNAP_TO_50_50));
+        mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50));
     }
 
     private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index af68442..7f61186 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -592,8 +592,9 @@
                     public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                             boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
                         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                                "onActivityRestartAttempt: %s", task.topActivity);
-                        if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
+                                "onActivityRestartAttempt: topActivity=%s, wasVisible=%b",
+                                task.topActivity, wasVisible);
+                        if (task.getWindowingMode() != WINDOWING_MODE_PINNED || !wasVisible) {
                             return;
                         }
                         if (mPipTaskOrganizer.isLaunchToSplit(task)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index d3bed59..a2439a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -361,8 +361,11 @@
                     final int anim = getRotationAnimationHint(change, info, mDisplayController);
                     isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
                     if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
-                        startRotationAnimation(startTransaction, change, info, anim, animations,
-                                onAnimFinish);
+                        final int flags = wallpaperTransit != WALLPAPER_TRANSITION_NONE
+                                && Flags.commonSurfaceAnimator()
+                                ? ScreenRotationAnimation.FLAG_HAS_WALLPAPER : 0;
+                        startRotationAnimation(startTransaction, change, info, anim, flags,
+                                animations, onAnimFinish);
                         isDisplayRotationAnimationStarted = true;
                         continue;
                     }
@@ -414,7 +417,7 @@
                 if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY)
                         && change.getStartRotation() != change.getEndRotation()) {
                     startRotationAnimation(startTransaction, change, info,
-                            ROTATION_ANIMATION_ROTATE, animations, onAnimFinish);
+                            ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish);
                     continue;
                 }
             }
@@ -699,12 +702,12 @@
     }
 
     private void startRotationAnimation(SurfaceControl.Transaction startTransaction,
-            TransitionInfo.Change change, TransitionInfo info, int animHint,
+            TransitionInfo.Change change, TransitionInfo info, int animHint, int flags,
             ArrayList<Animator> animations, Runnable onAnimFinish) {
         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
         final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext,
                 mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
-                animHint);
+                animHint, flags);
         // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
         // content, and background color. The item of "animGroup" will be removed if the sub
         // animation is finished. Then if the list becomes empty, the rotation animation is done.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 5802e2c..1a04997 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -25,12 +25,9 @@
 import static com.android.wm.shell.transition.Transitions.TAG;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.content.Context;
-import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
@@ -38,6 +35,7 @@
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
+import android.view.animation.AccelerateInterpolator;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.window.ScreenCapture;
@@ -74,6 +72,7 @@
  */
 class ScreenRotationAnimation {
     static final int MAX_ANIMATION_DURATION = 10 * 1000;
+    static final int FLAG_HAS_WALLPAPER = 1;
 
     private final Context mContext;
     private final TransactionPool mTransactionPool;
@@ -98,6 +97,12 @@
     private SurfaceControl mBackColorSurface;
     /** The leash using to animate screenshot layer. */
     private final SurfaceControl mAnimLeash;
+    /**
+     * The container with background color for {@link #mSurfaceControl}. It is only created if
+     * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed).
+     * That prevents flickering of alpha blending.
+     */
+    private SurfaceControl mBackEffectSurface;
 
     // The current active animation to move from the old to the new rotated
     // state.  Which animation is run here will depend on the old and new
@@ -111,8 +116,8 @@
     /** Intensity of light/whiteness of the layout after rotation occurs. */
     private float mEndLuma;
 
-    ScreenRotationAnimation(Context context, TransactionPool pool,
-            Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) {
+    ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t,
+            TransitionInfo.Change change, SurfaceControl rootLeash, int animHint, int flags) {
         mContext = context;
         mTransactionPool = pool;
         mAnimHint = animHint;
@@ -170,11 +175,20 @@
                 }
                 hardwareBuffer.close();
             }
+            if ((flags & FLAG_HAS_WALLPAPER) != 0) {
+                mBackEffectSurface = new SurfaceControl.Builder()
+                        .setCallsite("ShellRotationAnimation").setParent(rootLeash)
+                        .setEffectLayer().setOpaque(true).setName("BackEffect").build();
+                t.reparent(mSurfaceControl, mBackEffectSurface)
+                        .setColor(mBackEffectSurface,
+                                new float[] {mStartLuma, mStartLuma, mStartLuma})
+                        .show(mBackEffectSurface);
+            }
 
             t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
             t.show(mAnimLeash);
             // Crop the real content in case it contains a larger child layer, e.g. wallpaper.
-            t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
+            t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight));
 
             if (!isCustomRotate()) {
                 mBackColorSurface = new SurfaceControl.Builder()
@@ -202,6 +216,11 @@
         return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT;
     }
 
+    /** Returns the surface which contains the real content to animate enter. */
+    private SurfaceControl getEnterSurface() {
+        return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl;
+    }
+
     private void setScreenshotTransform(SurfaceControl.Transaction t) {
         if (mScreenshotLayer == null) {
             return;
@@ -314,7 +333,11 @@
         } else {
             startDisplayRotation(animations, finishCallback, mainExecutor);
             startScreenshotRotationAnimation(animations, finishCallback, mainExecutor);
-            //startColorAnimation(mTransaction, animationScale);
+            if (mBackEffectSurface != null && mStartLuma > 0.1f) {
+                // Animate from the color of background to black for smooth alpha blending.
+                buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface,
+                        animationScale, finishCallback, mainExecutor);
+            }
         }
 
         return true;
@@ -322,7 +345,7 @@
 
     private void startDisplayRotation(@NonNull ArrayList<Animator> animations,
             @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
-        buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
+        buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback,
                 mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */,
                 null /* clipRect */, false /* isActivity */);
     }
@@ -341,40 +364,17 @@
                 null /* clipRect */, false /* isActivity */);
     }
 
-    private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) {
-        int colorTransitionMs = mContext.getResources().getInteger(
-                R.integer.config_screen_rotation_color_transition);
-        final float[] rgbTmpFloat = new float[3];
-        final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma);
-        final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma);
-        final long duration = colorTransitionMs * (long) animationScale;
-        final Transaction t = mTransactionPool.acquire();
-
-        final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
-        // Animation length is already expected to be scaled.
-        va.overrideDurationScale(1.0f);
-        va.setDuration(duration);
-        va.addUpdateListener(animation -> {
-            final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
-            final float fraction = currentPlayTime / va.getDuration();
-            applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t);
-        });
-        va.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
-                        t);
-                mTransactionPool.release(t);
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
-                        t);
-                mTransactionPool.release(t);
-            }
-        });
-        animExecutor.execute(va::start);
+    private void buildLumaAnimation(@NonNull ArrayList<Animator> animations,
+            float startLuma, float endLuma, SurfaceControl surface, float animationScale,
+            @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
+        final long durationMillis = (long) (mContext.getResources().getInteger(
+                R.integer.config_screen_rotation_color_transition) * animationScale);
+        final LumaAnimation animation = new LumaAnimation(durationMillis);
+        // Align the end with the enter animation.
+        animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis);
+        final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma);
+        DefaultSurfaceAnimator.buildSurfaceAnimation(animations, animation, finishCallback,
+                mTransactionPool, mainExecutor, adapter);
     }
 
     public void kill() {
@@ -389,21 +389,47 @@
         if (mBackColorSurface != null && mBackColorSurface.isValid()) {
             t.remove(mBackColorSurface);
         }
+        if (mBackEffectSurface != null && mBackEffectSurface.isValid()) {
+            t.remove(mBackEffectSurface);
+        }
         t.apply();
         mTransactionPool.release(t);
     }
 
-    private static void applyColor(int startColor, int endColor, float[] rgbFloat,
-            float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
-        final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
-                endColor);
-        Color middleColor = Color.valueOf(color);
-        rgbFloat[0] = middleColor.red();
-        rgbFloat[1] = middleColor.green();
-        rgbFloat[2] = middleColor.blue();
-        if (surface.isValid()) {
-            t.setColor(surface, rgbFloat);
+    /** A no-op wrapper to provide animation duration. */
+    private static class LumaAnimation extends Animation {
+        LumaAnimation(long durationMillis) {
+            setDuration(durationMillis);
         }
-        t.apply();
+    }
+
+    private static class LumaAnimationAdapter extends DefaultSurfaceAnimator.AnimationAdapter {
+        final float[] mColorArray = new float[3];
+        final float mStartLuma;
+        final float mEndLuma;
+        final AccelerateInterpolator mInterpolation;
+
+        LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) {
+            super(leash);
+            mStartLuma = startLuma;
+            mEndLuma = endLuma;
+            // Make the initial progress color lighter if the background is light. That avoids
+            // darker content when fading into the entering surface.
+            final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10);
+            Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor);
+            mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null;
+        }
+
+        @Override
+        void applyTransformation(ValueAnimator animator, long currentPlayTime) {
+            final float fraction = mInterpolation != null
+                    ? mInterpolation.getInterpolation(animator.getAnimatedFraction())
+                    : animator.getAnimatedFraction();
+            final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma);
+            mColorArray[0] = luma;
+            mColorArray[1] = luma;
+            mColorArray[2] = luma;
+            mTransaction.setColor(mLeash, mColorArray);
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml
index 40dbbac..c8df15d 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
index 85715db..706c632 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
index 6c903a2..7df1675 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
index 6c903a2..7df1675 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
index f69a90c..d87c179 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index b76d065..99969e7 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
index 041978c..19c3e40 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index bf040d2..7505860 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -24,6 +24,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="on"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="on"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 177e47a..c52d9dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -19,7 +19,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -136,7 +136,7 @@
     @Test
     public void testSetDivideRatio() {
         mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */);
-        mSplitLayout.setDivideRatio(SNAP_TO_50_50);
+        mSplitLayout.setDivideRatio(SNAP_TO_2_50_50);
         assertThat(mSplitLayout.getDividerPosition()).isEqualTo(
                 mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
index 0c3f98a..0c100fc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
@@ -30,7 +30,7 @@
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
@@ -136,7 +136,7 @@
         assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
         assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
         assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
-        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_50_50)
+        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
     }
 
     @Test
@@ -185,7 +185,7 @@
     private fun splitTasksGroupInfo(): GroupedRecentTaskInfo {
         val task1 = createTaskInfo(id = 1)
         val task2 = createTaskInfo(id = 2)
-        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_50_50)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
         return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds)
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 386253c..753d4cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -22,7 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -211,10 +211,10 @@
 
         // Verify only one update if the split info is the same
         SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1);
         SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2);
         verify(mRecentTasksController, times(1)).notifyRecentTasksChanged();
     }
@@ -246,9 +246,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -277,9 +277,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -339,7 +339,7 @@
         setRawList(t1, t2, t3, t4, t5);
 
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
 
         when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
@@ -449,7 +449,7 @@
 
         // Add a pair
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds);
         reset(mRecentTasksController);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
index 248393c..be8e6dc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
@@ -1,6 +1,6 @@
 package com.android.wm.shell.recents;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -46,21 +46,21 @@
     @Test
     public void testVerticalStacked() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertTrue(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalStacked() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertFalse(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(0, dividerBounds.left);
         assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top);
@@ -71,7 +71,7 @@
     @Test
     public void testVerticalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left);
         assertEquals(0, dividerBounds.top);
@@ -82,7 +82,7 @@
     @Test
     public void testEqualVerticalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH;
         assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01);
     }
@@ -90,7 +90,7 @@
     @Test
     public void testEqualHorizontalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH;
         assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
index 19c18be..ac96063 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
@@ -42,19 +42,44 @@
             SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
         )
         assertEquals(
-            "the value of SNAP_TO_30_70 should be 0",
+            "the value of SNAP_TO_2_33_66 should be 0",
             0,
-            SplitScreenConstants.SNAP_TO_30_70,
+            SplitScreenConstants.SNAP_TO_2_33_66,
         )
         assertEquals(
-            "the value of SNAP_TO_50_50 should be 1",
+            "the value of SNAP_TO_2_50_50 should be 1",
             1,
-            SplitScreenConstants.SNAP_TO_50_50,
+            SplitScreenConstants.SNAP_TO_2_50_50,
         )
         assertEquals(
-            "the value of SNAP_TO_70_30 should be 2",
+            "the value of SNAP_TO_2_66_33 should be 2",
             2,
-            SplitScreenConstants.SNAP_TO_70_30,
+            SplitScreenConstants.SNAP_TO_2_66_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_90_10 should be 3",
+            3,
+            SplitScreenConstants.SNAP_TO_2_90_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_10_90 should be 4",
+            4,
+            SplitScreenConstants.SNAP_TO_2_10_90,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_33_33_33 should be 5",
+            5,
+            SplitScreenConstants.SNAP_TO_3_33_33_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_45_45_10 should be 6",
+            6,
+            SplitScreenConstants.SNAP_TO_3_45_45_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_10_45_45 should be 7",
+            7,
+            SplitScreenConstants.SNAP_TO_3_10_45_45,
         )
     }
 }
\ No newline at end of file
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index 3ed33db..bb0fc41 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -5,6 +5,11 @@
     ctor public AppFunctionManager(android.content.Context);
     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>);
+    method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
+    method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
+    field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0
+    field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2
+    field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1
   }
 
   public abstract class AppFunctionService extends android.app.Service {
@@ -41,6 +46,7 @@
     field public static final String PROPERTY_RETURN_VALUE = "returnValue";
     field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
     field public static final int RESULT_DENIED = 1; // 0x1
+    field public static final int RESULT_DISABLED = 6; // 0x6
     field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
     field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
     field public static final int RESULT_OK = 0; // 0x0
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 815fe05..d660926 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -16,11 +16,18 @@
 
 package com.google.android.appfunctions.sidecar;
 
+import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.UserHandleAware;
 import android.content.Context;
 import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -37,6 +44,39 @@
 // TODO(b/357551503): Implement get and set enabled app function APIs.
 // TODO(b/367329899): Add sidecar library to Android B builds.
 public final class AppFunctionManager {
+    /**
+     * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset
+     * enabled state to the default value.
+     */
+    public static final int APP_FUNCTION_STATE_DEFAULT = 0;
+
+    /**
+     * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled}
+     * with this value.
+     */
+    public static final int APP_FUNCTION_STATE_ENABLED = 1;
+
+    /**
+     * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled}
+     * with this value.
+     */
+    public static final int APP_FUNCTION_STATE_DISABLED = 2;
+
+    /**
+     * The enabled state of the app function.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"APP_FUNCTION_STATE_"},
+            value = {
+                    APP_FUNCTION_STATE_DEFAULT,
+                    APP_FUNCTION_STATE_ENABLED,
+                    APP_FUNCTION_STATE_DISABLED
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EnabledState {}
+
     private final android.app.appfunctions.AppFunctionManager mManager;
     private final Context mContext;
 
@@ -111,4 +151,64 @@
                 new CancellationSignal(),
                 callback);
     }
+
+    /**
+     * Returns a boolean through a callback, indicating whether the app function is enabled.
+     *
+     * <p>* This method can only check app functions owned by the caller, or those where the caller
+     * has visibility to the owner package and holds either the {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link
+     * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission.
+     *
+     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+     *
+     * <ul>
+     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+     *       have access to it.
+     * </ul>
+     *
+     * @param functionIdentifier the identifier of the app function to check (unique within the
+     *     target package) and in most cases, these are automatically generated by the AppFunctions
+     *     SDK
+     * @param targetPackage the package name of the app function's owner
+     * @param executor the executor to run the request
+     * @param callback the callback to receive the function enabled check result
+     */
+    public void isAppFunctionEnabled(
+            @NonNull String functionIdentifier,
+            @NonNull String targetPackage,
+            @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Boolean, Exception> callback) {
+        mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback);
+    }
+
+    /**
+     * Sets the enabled state of the app function owned by the calling package.
+     *
+     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
+     *
+     * <ul>
+     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
+     *       have access to it.
+     * </ul>
+     *
+     * @param functionIdentifier the identifier of the app function to enable (unique within the
+     *     calling package). In most cases, identifiers are automatically generated by the
+     *     AppFunctions SDK
+     * @param newEnabledState the new state of the app function
+     * @param executor the executor to run the callback
+     * @param callback the callback to receive the result of the function enablement. The call was
+     *     successful if no exception was thrown.
+     */
+    // Constants in @EnabledState should always mirror those in
+    // android.app.appfunctions.AppFunctionManager.
+    @SuppressLint("WrongConstant")
+    @UserHandleAware
+    public void setAppFunctionEnabled(
+            @NonNull String functionIdentifier,
+            @EnabledState int newEnabledState,
+            @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Void, Exception> callback) {
+        mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback);
+    }
 }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
index 60c25fa..c7ce95b 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -76,6 +76,9 @@
     /** The operation was timed out. */
     public static final int RESULT_TIMED_OUT = 5;
 
+    /** The caller tried to execute a disabled app function. */
+    public static final int RESULT_DISABLED = 6;
+
     /** The result code of the app function execution. */
     @ResultCode private final int mResultCode;
 
@@ -234,6 +237,7 @@
                     RESULT_INTERNAL_ERROR,
                     RESULT_INVALID_ARGUMENT,
                     RESULT_TIMED_OUT,
+                    RESULT_DISABLED
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ResultCode {}
diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp
index bbb1420..f0bcfe53 100644
--- a/libs/hwui/hwui/MinikinSkia.cpp
+++ b/libs/hwui/hwui/MinikinSkia.cpp
@@ -36,7 +36,7 @@
 
 MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData,
                                  size_t fontSize, std::string_view filePath, int ttcIndex,
-                                 const std::vector<minikin::FontVariation>& axes)
+                                 const minikin::VariationSettings& axes)
         : mTypeface(std::move(typeface))
         , mSourceId(sourceId)
         , mFontData(fontData)
@@ -123,12 +123,12 @@
     return mTtcIndex;
 }
 
-const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const {
+const minikin::VariationSettings& MinikinFontSkia::GetAxes() const {
     return mAxes;
 }
 
 std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation(
-        const std::vector<minikin::FontVariation>& variations) const {
+        const minikin::VariationSettings& variations) const {
     SkFontArguments args;
 
     std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation;
diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h
index de9a5c2..7fe5978bfd 100644
--- a/libs/hwui/hwui/MinikinSkia.h
+++ b/libs/hwui/hwui/MinikinSkia.h
@@ -32,7 +32,7 @@
 public:
     MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize,
                     std::string_view filePath, int ttcIndex,
-                    const std::vector<minikin::FontVariation>& axes);
+                    const minikin::VariationSettings& axes);
 
     float GetHorizontalAdvance(uint32_t glyph_id, const minikin::MinikinPaint& paint,
                                const minikin::FontFakery& fakery) const override;
@@ -59,9 +59,9 @@
     size_t GetFontSize() const;
     int GetFontIndex() const;
     const std::string& getFilePath() const { return mFilePath; }
-    const std::vector<minikin::FontVariation>& GetAxes() const;
+    const minikin::VariationSettings& GetAxes() const;
     std::shared_ptr<minikin::MinikinFont> createFontWithVariation(
-            const std::vector<minikin::FontVariation>&) const;
+            const minikin::VariationSettings&) const;
     int GetSourceId() const override { return mSourceId; }
 
     static uint32_t packFontFlags(const SkFont&);
@@ -80,7 +80,7 @@
     const void* mFontData;
     size_t mFontSize;
     int mTtcIndex;
-    std::vector<minikin::FontVariation> mAxes;
+    minikin::VariationSettings mAxes;
     std::string mFilePath;
 };
 
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index a9d1a2a..2d812d6 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -92,8 +92,8 @@
     return result;
 }
 
-Typeface* Typeface::createFromTypefaceWithVariation(
-        Typeface* src, const std::vector<minikin::FontVariation>& variations) {
+Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src,
+                                                    const minikin::VariationSettings& variations) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
     Typeface* result = new Typeface();
     if (result != nullptr) {
@@ -192,9 +192,8 @@
     sk_sp<SkTypeface> typeface = fm->makeFromStream(std::move(fontData));
     LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", kRobotoFont);
 
-    std::shared_ptr<minikin::MinikinFont> font =
-            std::make_shared<MinikinFontSkia>(std::move(typeface), 0, data, st.st_size, kRobotoFont,
-                                              0, std::vector<minikin::FontVariation>());
+    std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>(
+            std::move(typeface), 0, data, st.st_size, kRobotoFont, 0, minikin::VariationSettings());
     std::vector<std::shared_ptr<minikin::Font>> fonts;
     fonts.push_back(minikin::Font::Builder(font).build());
 
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 565136e..2c96c1a 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -74,8 +74,8 @@
     static Typeface* createRelative(Typeface* src, Style desiredStyle);
     static Typeface* createAbsolute(Typeface* base, int weight, bool italic);
 
-    static Typeface* createFromTypefaceWithVariation(
-            Typeface* src, const std::vector<minikin::FontVariation>& variations);
+    static Typeface* createFromTypefaceWithVariation(Typeface* src,
+                                                     const minikin::VariationSettings& variations);
 
     static Typeface* createFromFamilies(
             std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic,
diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp
index e6d790f..9922ff3 100644
--- a/libs/hwui/jni/FontFamily.cpp
+++ b/libs/hwui/jni/FontFamily.cpp
@@ -133,9 +133,9 @@
         builder->axes.clear();
         return false;
     }
-    std::shared_ptr<minikin::MinikinFont> minikinFont =
-            std::make_shared<MinikinFontSkia>(std::move(face), fonts::getNewSourceId(), fontPtr,
-                                              fontSize, "", ttcIndex, builder->axes);
+    std::shared_ptr<minikin::MinikinFont> minikinFont = std::make_shared<MinikinFontSkia>(
+            std::move(face), fonts::getNewSourceId(), fontPtr, fontSize, "", ttcIndex,
+            minikin::VariationSettings(builder->axes, false));
     minikin::Font::Builder fontBuilder(minikinFont);
 
     if (weight != RESOLVE_BY_FONT_TABLE) {
diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp
index 3884342..e9de655 100644
--- a/libs/hwui/jni/PathIterator.cpp
+++ b/libs/hwui/jni/PathIterator.cpp
@@ -20,6 +20,7 @@
 #include "GraphicsJNI.h"
 #include "SkPath.h"
 #include "SkPoint.h"
+#include "graphics_jni_helpers.h"
 
 namespace android {
 
@@ -36,6 +37,18 @@
         return reinterpret_cast<jlong>(new SkPath::RawIter(*path));
     }
 
+    // A variant of 'next' (below) that is compatible with the host JVM.
+    static jint nextHost(JNIEnv* env, jclass clazz, jlong iteratorHandle, jfloatArray pointsArray) {
+        jfloat* points = env->GetFloatArrayElements(pointsArray, 0);
+#ifdef __ANDROID__
+        jint result = next(iteratorHandle, reinterpret_cast<jlong>(points));
+#else
+        jint result = next(env, clazz, iteratorHandle, reinterpret_cast<jlong>(points));
+#endif
+        env->ReleaseFloatArrayElements(pointsArray, points, 0);
+        return result;
+    }
+
     // ---------------- @CriticalNative -------------------------
 
     static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) {
@@ -72,6 +85,7 @@
 
         {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek},
         {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next},
+        {"nNextHost", "(J[F)I", (void*)SkPathIteratorGlue::nextHost},
 };
 
 int register_android_graphics_PathIterator(JNIEnv* env) {
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index 209b35c..0f458dd 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -80,7 +80,8 @@
         AxisHelper axis(env, axisObject);
         variations.push_back(minikin::FontVariation(axis.getTag(), axis.getStyleValue()));
     }
-    return toJLong(Typeface::createFromTypefaceWithVariation(toTypeface(familyHandle), variations));
+    return toJLong(Typeface::createFromTypefaceWithVariation(
+            toTypeface(familyHandle), minikin::VariationSettings(variations, false /* sorted */)));
 }
 
 static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) {
@@ -273,7 +274,7 @@
     const std::string& path = typeface->GetFontPath();
     writer->writeString(path);
     writer->write<int>(typeface->GetFontIndex());
-    const std::vector<minikin::FontVariation>& axes = typeface->GetAxes();
+    const minikin::VariationSettings& axes = typeface->GetAxes();
     writer->writeArray<minikin::FontVariation>(axes.data(), axes.size());
     bool hasVerity = getVerity(path);
     writer->write<int8_t>(static_cast<int8_t>(hasVerity));
diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp
index f405aba..6a05b6c 100644
--- a/libs/hwui/jni/fonts/Font.cpp
+++ b/libs/hwui/jni/fonts/Font.cpp
@@ -142,7 +142,7 @@
     std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>(
             std::move(newTypeface), minikinSkia->GetSourceId(), minikinSkia->GetFontData(),
             minikinSkia->GetFontSize(), minikinSkia->getFilePath(), minikinSkia->GetFontIndex(),
-            builder->axes);
+            minikin::VariationSettings(builder->axes, false));
     std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont)
               .setWeight(weight)
               .setSlant(static_cast<minikin::FontStyle::Slant>(italic))
@@ -303,7 +303,7 @@
         var = reader.readArray<minikin::FontVariation>().first[index];
     } else {
         const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->baseTypeface();
-        var = minikinFont->GetAxes().at(index);
+        var = minikinFont->GetAxes()[index];
     }
     uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value);
     return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary);
diff --git a/native/android/system_fonts.cpp b/native/android/system_fonts.cpp
index 91f78ce..0c07b2a 100644
--- a/native/android/system_fonts.cpp
+++ b/native/android/system_fonts.cpp
@@ -327,7 +327,7 @@
     result->mWeight = font->style().weight();
     result->mItalic = font->style().slant() == minikin::FontStyle::Slant::ITALIC;
     result->mCollectionIndex = minikinFontSkia->GetFontIndex();
-    const std::vector<minikin::FontVariation>& axes = minikinFontSkia->GetAxes();
+    const minikin::VariationSettings& axes = minikinFontSkia->GetAxes();
     result->mAxes.reserve(axes.size());
     for (auto axis : axes) {
         result->mAxes.push_back(std::make_pair(axis.axisTag, axis.value));
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index a36ec00..e77c2a0 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -43,6 +43,7 @@
         "SettingsLibFooterPreference",
         "SettingsLibHelpUtils",
         "SettingsLibIllustrationPreference",
+        "SettingsLibIntroPreference",
         "SettingsLibLayoutPreference",
         "SettingsLibMainSwitchPreference",
         "SettingsLibProfileSelector",
diff --git a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml
index 47ce587..b06052a 100644
--- a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml
+++ b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml
@@ -78,15 +78,6 @@
             android:textAppearance="?android:attr/textAppearanceSmall"
             android:textColor="?android:attr/textColorSecondary"
             android:visibility="gone"/>
-
-        <ProgressBar
-            android:id="@android:id/progress"
-            style="?android:attr/progressBarStyleHorizontal"
-            android:layout_width="match_parent"
-            android:layout_height="4dp"
-            android:layout_marginTop="4dp"
-            android:max="100"
-            android:visibility="gone"/>
     </LinearLayout>
 
     <LinearLayout
diff --git a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml
index e65f7de..ac57228 100644
--- a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml
+++ b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml
@@ -74,15 +74,6 @@
             android:textAppearance="?android:attr/textAppearanceSmall"
             android:textColor="?android:attr/textColorSecondary"
             android:visibility="gone"/>
-
-        <ProgressBar
-            android:id="@android:id/progress"
-            style="?android:attr/progressBarStyleHorizontal"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="4dp"
-            android:max="100"
-            android:visibility="gone"/>
     </LinearLayout>
 
     <LinearLayout
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
index f1d162e..3b52df7 100644
--- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
+++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
@@ -18,11 +18,8 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ProgressBar;
 
 import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
 
 import com.android.settingslib.widget.preference.app.R;
 
@@ -31,9 +28,6 @@
  */
 public class AppPreference extends Preference {
 
-    private int mProgress;
-    private boolean mProgressVisible;
-
     public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         setLayoutResource(R.layout.preference_app);
@@ -53,29 +47,4 @@
         super(context, attrs);
         setLayoutResource(R.layout.preference_app);
     }
-
-    /**
-     * Sets the current progress.
-     * @param amount the current progress
-     *
-     * @see ProgressBar#setProgress(int)
-     */
-    public void setProgress(int amount) {
-        mProgress = amount;
-        mProgressVisible = true;
-        notifyChanged();
-    }
-
-    @Override
-    public void onBindViewHolder(PreferenceViewHolder view) {
-        super.onBindViewHolder(view);
-
-        final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress);
-        if (mProgressVisible) {
-            progress.setProgress(mProgress);
-            progress.setVisibility(View.VISIBLE);
-        } else {
-            progress.setVisibility(View.GONE);
-        }
-    }
 }
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml
new file mode 100644
index 0000000..f55b320
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Filled" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml
new file mode 100644
index 0000000..b663b6c
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Filled.Extra" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml
new file mode 100644
index 0000000..784e6ad
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Filled.Large" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml
new file mode 100644
index 0000000..8b44a65
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Outline" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml
new file mode 100644
index 0000000..f8a2d8f
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Outline.Extra" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml
new file mode 100644
index 0000000..781a5a1
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Outline.Large" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml
new file mode 100644
index 0000000..5b568f8
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Tonal" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml
new file mode 100644
index 0000000..1e7a08b
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Tonal.Extra" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml
new file mode 100644
index 0000000..42116be
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml
@@ -0,0 +1,29 @@
+<?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:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settingslib_button"
+        style="@style/SettingsLibButtonStyle.Expressive.Tonal.Large" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
new file mode 100644
index 0000000..a1761e5
--- /dev/null
+++ b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
@@ -0,0 +1,31 @@
+<?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="ButtonPreference">
+        <attr name="buttonType" format="enum">
+            <enum name="filled" value="0"/>
+            <enum name="tonal" value="1"/>
+            <enum name="outline" value="2"/>
+        </attr>
+        <attr name="buttonSize" format="enum">
+            <enum name="normal" value="0"/>
+            <enum name="large" value="1"/>
+            <enum name="extra" value="2"/>
+        </attr>
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
index 16ba962..0041eb2 100644
--- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
+++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
@@ -32,11 +32,44 @@
 
 import com.android.settingslib.widget.preference.button.R;
 
+import com.google.android.material.button.MaterialButton;
+
 /**
  * A preference handled a button
  */
 public class ButtonPreference extends Preference {
 
+    enum ButtonStyle {
+        FILLED_NORMAL(0, 0, R.layout.settingslib_expressive_button_filled),
+        FILLED_LARGE(0, 1, R.layout.settingslib_expressive_button_filled_large),
+        FILLED_EXTRA(0, 2, R.layout.settingslib_expressive_button_filled_extra),
+        TONAL_NORMAL(1, 0, R.layout.settingslib_expressive_button_tonal),
+        TONAL_LARGE(1, 1, R.layout.settingslib_expressive_button_tonal_large),
+        TONAL_EXTRA(1, 2, R.layout.settingslib_expressive_button_tonal_extra),
+        OUTLINE_NORMAL(2, 0, R.layout.settingslib_expressive_button_outline),
+        OUTLINE_LARGE(2, 1, R.layout.settingslib_expressive_button_outline_large),
+        OUTLINE_EXTRA(2, 2, R.layout.settingslib_expressive_button_outline_extra);
+
+        private final int mType;
+        private final int mSize;
+        private final int mLayoutId;
+
+        ButtonStyle(int type, int size, int layoutId) {
+            this.mType = type;
+            this.mSize = size;
+            this.mLayoutId = layoutId;
+        }
+
+        static int getLayoutId(int type, int size) {
+            for (ButtonStyle style : values()) {
+                if (style.mType == type && style.mSize == size) {
+                    return style.mLayoutId;
+                }
+            }
+            throw new IllegalArgumentException();
+        }
+    }
+
     private static final int ICON_SIZE = 24;
 
     private View.OnClickListener mClickListener;
@@ -86,7 +119,7 @@
     }
 
     private void init(Context context, AttributeSet attrs, int defStyleAttr) {
-        setLayoutResource(R.layout.settingslib_button_layout);
+        int resId = R.layout.settingslib_button_layout;
 
         if (attrs != null) {
             TypedArray a = context.obtainStyledAttributes(attrs,
@@ -102,8 +135,16 @@
                     R.styleable.ButtonPreference, defStyleAttr,
                     0 /*defStyleRes*/);
             mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START);
+
+            if (SettingsThemeHelper.isExpressiveTheme(context)) {
+                int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0);
+                int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0);
+                resId = ButtonStyle.getLayoutId(type, size);
+            }
             a.recycle();
         }
+
+        setLayoutResource(resId);
     }
 
     @Override
@@ -144,14 +185,20 @@
         if (mButton == null || icon == null) {
             return;
         }
-        //get pixel from dp
-        int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE,
-                getContext().getResources().getDisplayMetrics());
-        icon.setBounds(0, 0, size, size);
 
-        //set drawableStart
-        mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */, null/* end */,
-                null/* bottom */);
+        if (mButton instanceof MaterialButton) {
+            ((MaterialButton) mButton).setIcon(icon);
+        } else {
+            //get pixel from dp
+            int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE,
+                    getContext().getResources().getDisplayMetrics());
+            icon.setBounds(0, 0, size, size);
+
+            //set drawableStart
+            mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */,
+                    null/* end */,
+                    null/* bottom */);
+        }
     }
 
     @Override
diff --git a/packages/SettingsLib/IntroPreference/Android.bp b/packages/SettingsLib/IntroPreference/Android.bp
new file mode 100644
index 0000000..155db18
--- /dev/null
+++ b/packages/SettingsLib/IntroPreference/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: "SettingsLibIntroPreference",
+    use_resource_processor: true,
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
+    ],
+
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+    apex_available: [
+        "//apex_available:platform",
+    ],
+}
diff --git a/packages/SettingsLib/IntroPreference/AndroidManifest.xml b/packages/SettingsLib/IntroPreference/AndroidManifest.xml
new file mode 100644
index 0000000..f1bfee5
--- /dev/null
+++ b/packages/SettingsLib/IntroPreference/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.intro">
+
+    <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml
new file mode 100644
index 0000000..203a395
--- /dev/null
+++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/entity_header"
+    style="@style/SettingsLibEntityHeader">
+
+    <LinearLayout
+        android:id="@+id/entity_header_content"
+        style="@style/SettingsLibEntityHeaderContent">
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:src="@drawable/settingslib_arrow_drop_down"
+            style="@style/SettingsLibEntityHeaderIcon"/>
+
+        <TextView
+            android:id="@android:id/title"
+            android:text="Title"
+            style="@style/SettingsLibEntityHeaderTitle"/>
+
+        <com.android.settingslib.widget.CollapsableTextView
+            android:id="@+id/collapsable_summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"/>
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
new file mode 100644
index 0000000..c93ec2b
--- /dev/null
+++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.intro.R
+
+class IntroPreference @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 {
+        layoutResource = R.layout.settingslib_expressive_preference_intro
+        isSelectable = false
+
+        initAttributes(context, attrs, defStyleAttr)
+    }
+
+    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.isDividerAllowedBelow = false
+        holder.isDividerAllowedAbove = false
+
+        (holder.findViewById(R.id.collapsable_summary) as? CollapsableTextView)?.apply {
+            setCollapsable(isCollapsable)
+            setMinLines(minLines)
+            setText(summary.toString())
+        }
+    }
+
+    /**
+     * Sets whether the summary is collapsable.
+     * @param collapsable True if the summary should be collapsable, false otherwise.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    fun setCollapsable(collapsable: Boolean) {
+        isCollapsable = collapsable
+        minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
+        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
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
index 17852e8..e83e17c 100644
--- a/packages/SettingsLib/Preference/Android.bp
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -22,3 +22,16 @@
     ],
     kotlincflags: ["-Xjvm-default=all"],
 }
+
+android_library {
+    name: "SettingsLibPreference-testutils",
+    srcs: ["testutils/**/*.kt"],
+    static_libs: [
+        "SettingsLibPreference",
+        "androidx.fragment_fragment-testing",
+        "androidx.test.core",
+        "androidx.test.ext.junit",
+        "flag-junit",
+        "truth",
+    ],
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
index 5fcf478..5e69895 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -68,10 +68,13 @@
                 preference.icon = null
             }
             val context = preference.context
+            val isPreferenceScreen = preference is PreferenceScreen
             preference.peekExtras()?.clear()
             extras(context)?.let { preference.extras.putAll(it) }
             preference.title = getPreferenceTitle(context)
-            preference.summary = getPreferenceSummary(context)
+            if (!isPreferenceScreen) {
+                preference.summary = getPreferenceSummary(context)
+            }
             preference.isEnabled = isEnabled(context)
             preference.isVisible =
                 (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
@@ -81,7 +84,7 @@
             // dependency here. This simplifies dependency management and avoid the
             // IllegalStateException when call Preference.setDependency
             preference.dependency = null
-            if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+            if (!isPreferenceScreen) { // avoid recursive loop when build graph
                 preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
                 preference.intent = intent(context)
             }
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
new file mode 100644
index 0000000..4d5f85f
--- /dev/null
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.preference
+
+import android.content.Context
+import android.platform.test.flag.junit.SetFlagsRule
+import android.util.Log
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Test case for catalyst screen. */
+@RunWith(AndroidJUnit4::class)
+abstract class CatalystScreenTestCase {
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    protected val context: Context = ApplicationProvider.getApplicationContext()
+
+    /** Catalyst screen. */
+    protected abstract val preferenceScreenCreator: PreferenceScreenCreator
+
+    /** Flag to control catalyst screen. */
+    protected abstract val flagName: String
+
+    /**
+     * Test to compare the preference screen hierarchy between legacy screen (flag is disabled) and
+     * catalyst screen (flag is enabled).
+     */
+    @Test
+    fun migration() {
+        enableCatalystScreen()
+        assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue()
+        val catalystScreen = stringifyPreferenceScreen()
+        Log.i("Catalyst", catalystScreen)
+
+        disableCatalystScreen()
+        assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse()
+        val legacyScreen = stringifyPreferenceScreen()
+
+        assertThat(catalystScreen).isEqualTo(legacyScreen)
+    }
+
+    /**
+     * Enables the catalyst screen.
+     *
+     * By default, enable the [flagName]. Override for more complex situation.
+     */
+    @Suppress("DEPRECATION")
+    protected open fun enableCatalystScreen() {
+        setFlagsRule.enableFlags(flagName)
+    }
+
+    /**
+     * Disables the catalyst screen (legacy screen is shown).
+     *
+     * By default, disable the [flagName]. Override for more complex situation.
+     */
+    @Suppress("DEPRECATION")
+    protected open fun disableCatalystScreen() {
+        setFlagsRule.disableFlags(flagName)
+    }
+
+    private fun stringifyPreferenceScreen(): String {
+        @Suppress("UNCHECKED_CAST")
+        val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat>
+        val builder = StringBuilder()
+        FragmentScenario.launch(clazz).use {
+            it.onFragment { fragment -> fragment.preferenceScreen.toString(builder) }
+        }
+        return builder.toString()
+    }
+
+    private fun Preference.toString(builder: StringBuilder, indent: String = "") {
+        val clazz = javaClass
+        builder.append(indent).append(clazz).append(" {\n")
+        val indent2 = "$indent  "
+        if (clazz != PreferenceScreen::class.java) {
+            key?.let { builder.append(indent2).append("key: \"$it\"\n") }
+        }
+        title?.let { builder.append(indent2).append("title: \"$it\"\n") }
+        summary?.let { builder.append(indent2).append("summary: \"$it\"\n") }
+        fragment?.let { builder.append(indent2).append("fragment: \"$it\"\n") }
+        builder.append(indent2).append("order: $order\n")
+        builder.append(indent2).append("isCopyingEnabled: $isCopyingEnabled\n")
+        builder.append(indent2).append("isEnabled: $isEnabled\n")
+        builder.append(indent2).append("isIconSpaceReserved: $isIconSpaceReserved\n")
+        if (clazz != Preference::class.java && clazz != PreferenceScreen::class.java) {
+            builder.append(indent2).append("isPersistent: $isPersistent\n")
+        }
+        builder.append(indent2).append("isSelectable: $isSelectable\n")
+        if (this is PreferenceGroup) {
+            val count = preferenceCount
+            builder.append(indent2).append("preferenceCount: $count\n")
+            val indent4 = "$indent2  "
+            for (index in 0..<count) {
+                getPreference(index).toString(builder, indent4)
+            }
+        }
+        builder.append(indent).append("}\n")
+    }
+}
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 250c27e..9c65905 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -290,4 +290,43 @@
         <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item>
         <item name="rippleColor">?android:attr/colorControlHighlight</item>
     </style>
+
+    <style name="EntityHeader">
+        <item name="android:paddingTop">@dimen/settingslib_expressive_space_small4</item>
+        <item name="android:paddingBottom">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:paddingEnd">@dimen/settingslib_expressive_space_small1</item>
+    </style>
+
+    <style name="SettingsLibEntityHeader" parent="EntityHeader">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item>
+        <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item>
+    </style>
+
+    <style name="SettingsLibEntityHeaderContent">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_centerHorizontal">true</item>
+        <item name="android:orientation">vertical</item>
+        <item name="android:gravity">center_horizontal</item>
+    </style>
+
+    <style name="SettingsLibEntityHeaderIcon">
+        <item name="android:layout_width">@dimen/settingslib_expressive_space_large3</item>
+        <item name="android:layout_height">@dimen/settingslib_expressive_space_large3</item>
+        <item name="android:scaleType">fitCenter</item>
+        <item name="android:antialias">true</item>
+    </style>
+
+    <style name="SettingsLibEntityHeaderTitle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginTop">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:singleLine">false</item>
+        <item name="android:gravity">center</item>
+        <item name="android:ellipsize">marquee</item>
+        <item name="android:textDirection">locale</item>
+        <item name="android:textAppearance">@style/TextAppearance.EntityHeaderTitle</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 7139f5b4..2a251a5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -30,9 +30,6 @@
 import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider
 import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
-import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
-import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider
-import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
 import com.android.settingslib.spa.gallery.editor.SettingsOutlinedTextFieldPageProvider
 import com.android.settingslib.spa.gallery.editor.SettingsTextFieldPasswordPageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
@@ -66,10 +63,6 @@
  */
 enum class SettingsPageProviderEnum(val displayName: String) {
     HOME("home"),
-    PREFERENCE("preference"),
-    ARGUMENT("argument"),
-    ITEM_LIST("itemList"),
-    ITEM_OP_PAGE("itemOp"),
 
     // Add your SPPs
 }
@@ -101,9 +94,6 @@
                 ChartPageProvider,
                 DialogMainPageProvider,
                 NavDialogProvider,
-                ItemListPageProvider,
-                ItemOperatePageProvider,
-                OperateListPageProvider,
                 EditorMainPageProvider,
                 SettingsOutlinedTextFieldPageProvider,
                 SettingsDropdownBoxPageProvider,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt
index 6edd917..c16d8bf 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt
@@ -39,9 +39,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -161,14 +159,12 @@
         }
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     private const val TITLE = "Sample Banner"
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt
index b001cad..773d3d1 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt
@@ -23,9 +23,7 @@
 import androidx.compose.material.icons.outlined.WarningAmber
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.button.ActionButton
@@ -55,14 +53,12 @@
         }
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt
index 7a6ae2c..6ceb395 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt
@@ -39,6 +39,7 @@
 private enum class WeekDay(val num: Int) {
     Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6),
 }
+
 private const val TITLE = "Sample Chart"
 
 object ChartPageProvider : SettingsPageProvider {
@@ -103,14 +104,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
index 4e3fcee..c9c81aa 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
@@ -18,6 +18,7 @@
 
 import android.os.Bundle
 import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -55,13 +56,13 @@
         }.build(),
     )
 
-    fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner)
-        .setUiLayoutFn {
-            Preference(object : PreferenceModel {
-                override val title = TITLE
-                override val onClick = navigator(name)
-            })
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
+    }
 
     override fun getTitle(arguments: Bundle?) = TITLE
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
index c511542..f2b4091 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
@@ -17,8 +17,8 @@
 package com.android.settingslib.spa.gallery.editor
 
 import android.os.Bundle
+import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
@@ -44,14 +44,12 @@
         )
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index b1558cc..4d77ea1 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -20,20 +20,16 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.banner.BannerPageProvider
+import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.chart.ChartPageProvider
 import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
-import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
-import com.android.settingslib.spa.gallery.page.ArgumentPageModel
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
@@ -48,35 +44,11 @@
 import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
 import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
 import com.android.settingslib.spa.widget.scaffold.HomeScaffold
+import com.android.settingslib.spa.widget.ui.Category
 
 object HomePageProvider : SettingsPageProvider {
     override val name = SettingsPageProviderEnum.HOME.name
     override val displayName = SettingsPageProviderEnum.HOME.displayName
-    private val owner = createSettingsPage()
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        return listOf(
-            PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            OperateListPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(),
-            SearchScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            SuwScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            PagerMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            DialogMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            BannerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-        )
-    }
 
     override fun getTitle(arguments: Bundle?): String {
         return SpaEnvironmentFactory.instance.appContext.getString(R.string.app_name)
@@ -85,14 +57,30 @@
     @Composable
     override fun Page(arguments: Bundle?) {
         val title = remember { getTitle(arguments) }
-        val entries = remember { buildEntry(arguments) }
         HomeScaffold(title) {
-            for (entry in entries) {
-                if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) {
-                    entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0))
-                } else {
-                    entry.UiLayout()
-                }
+            Category {
+                PreferenceMainPageProvider.Entry()
+            }
+            Category {
+                SearchScaffoldPageProvider.Entry()
+                SuwScaffoldPageProvider.Entry()
+                ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0)
+            }
+            Category {
+                SliderPageProvider.Entry()
+                SpinnerPageProvider.Entry()
+                PagerMainPageProvider.Entry()
+                FooterPageProvider.Entry()
+                IllustrationPageProvider.Entry()
+                CategoryPageProvider.Entry()
+                ActionButtonPageProvider.Entry()
+                ProgressBarPageProvider.Entry()
+                LoadingBarPageProvider.Entry()
+                ChartPageProvider.Entry()
+                DialogMainPageProvider.Entry()
+                EditorMainPageProvider.Entry()
+                BannerPageProvider.Entry()
+                CopyablePageProvider.Entry()
             }
         }
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt
deleted file mode 100644
index 5f251b1..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery.itemList
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.core.os.bundleOf
-import androidx.navigation.NavType
-import androidx.navigation.navArgument
-import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.util.getStringArg
-import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
-
-private const val OPERATOR_PARAM_NAME = "opParam"
-
-object ItemListPageProvider : SettingsPageProvider {
-    override val name = SettingsPageProviderEnum.ITEM_LIST.name
-    override val displayName = SettingsPageProviderEnum.ITEM_LIST.displayName
-    override val parameter = listOf(
-        navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType },
-    )
-
-    override fun getTitle(arguments: Bundle?): String {
-        val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "NULL"
-        return "Operation: $operation"
-    }
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        if (!ItemOperatePageProvider.isValidArgs(arguments)) return emptyList()
-        val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!!
-        val owner = createSettingsPage(arguments)
-        return listOf(
-            ItemOperatePageProvider.buildInjectEntry(operation)!!.setLink(fromPage = owner).build(),
-        )
-    }
-
-    fun buildInjectEntry(opParam: String): SettingsEntryBuilder? {
-        val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam)
-        if (!ItemOperatePageProvider.isValidArgs(arguments)) return null
-
-        return SettingsEntryBuilder.createInject(
-            owner = createSettingsPage(arguments),
-            label = "ItemList_$opParam",
-        ).setUiLayoutFn {
-            Preference(
-                object : PreferenceModel {
-                    override val title = opParam
-                    override val onClick = navigator(
-                        SettingsPageProviderEnum.ITEM_LIST.name + parameter.navLink(it)
-                    )
-                }
-            )
-        }.setSearchDataFn {
-            EntrySearchData(title = "Operation: $opParam")
-        }
-    }
-
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        val title = remember { getTitle(arguments) }
-        val entries = remember { buildEntry(arguments) }
-        val itemList = remember {
-            // Add logic to get item List during runtime.
-            listOf("itemFoo", "itemBar", "itemToy")
-        }
-        RegularScaffold(title) {
-            for (item in itemList) {
-                val rtArgs = ItemOperatePageProvider.genRuntimeArguments(item)
-                for (entry in entries) {
-                    entry.UiLayout(rtArgs)
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt
deleted file mode 100644
index 6caec07..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery.itemList
-
-import android.os.Bundle
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.core.os.bundleOf
-import androidx.navigation.NavType
-import androidx.navigation.navArgument
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.util.getStringArg
-import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.preference.SwitchPreference
-import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-
-private const val OPERATOR_PARAM_NAME = "opParam"
-private const val ITEM_NAME_PARAM_NAME = "rt_nameParam"
-private val ALLOWED_OPERATOR_LIST = listOf("opDnD", "opPiP", "opInstall", "opConnect")
-
-object ItemOperatePageProvider : SettingsPageProvider {
-    override val name = SettingsPageProviderEnum.ITEM_OP_PAGE.name
-    override val displayName = SettingsPageProviderEnum.ITEM_OP_PAGE.displayName
-    override val parameter = listOf(
-        navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType },
-        navArgument(ITEM_NAME_PARAM_NAME) { type = NavType.StringType },
-    )
-
-    override fun getTitle(arguments: Bundle?): String {
-        // Operation name is not a runtime parameter, which should always available
-        val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "opInValid"
-        // Item name is a runtime parameter, which could be missing
-        val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, arguments) ?: "[unset]"
-        return "$operation on $itemName"
-    }
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        if (!isValidArgs(arguments)) return emptyList()
-
-        val owner = createSettingsPage(arguments)
-        val entryList = mutableListOf<SettingsEntry>()
-        entryList.add(
-            SettingsEntryBuilder.create("ItemName", owner)
-                .setUiLayoutFn {
-                    // Item name is a runtime parameter, which needs to be read inside UiLayoutFn
-                    val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL"
-                    Preference(
-                        object : PreferenceModel {
-                            override val title = "Item $itemName"
-                        }
-                    )
-                }.build()
-        )
-
-        // Operation name is not a runtime parameter, which can be read outside.
-        val opName = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!!
-        entryList.add(
-            SettingsEntryBuilder.create("ItemOp", owner)
-                .setUiLayoutFn {
-                    var checked by rememberSaveable { mutableStateOf(false) }
-                    SwitchPreference(remember {
-                        object : SwitchPreferenceModel {
-                            override val title = "Item operation: $opName"
-                            override val checked = { checked }
-                            override val onCheckedChange =
-                                { newChecked: Boolean -> checked = newChecked }
-                        }
-                    })
-                }.build(),
-        )
-        return entryList
-    }
-
-    fun buildInjectEntry(opParam: String): SettingsEntryBuilder? {
-        val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam)
-        if (!isValidArgs(arguments)) return null
-
-        return SettingsEntryBuilder.createInject(
-            owner = createSettingsPage(arguments),
-            label = "ItemOp_$opParam",
-        ).setUiLayoutFn {
-            // Item name is a runtime parameter, which needs to be read inside UiLayoutFn
-            val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL"
-            Preference(
-                object : PreferenceModel {
-                    override val title = "item: $itemName"
-                    override val onClick = navigator(
-                        SettingsPageProviderEnum.ITEM_OP_PAGE.name + parameter.navLink(it)
-                    )
-                }
-            )
-        }
-    }
-
-    fun isValidArgs(arguments: Bundle?): Boolean {
-        val opParam = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)
-        return (opParam != null && ALLOWED_OPERATOR_LIST.contains(opParam))
-    }
-
-    fun genRuntimeArguments(itemName: String): Bundle {
-        return bundleOf(ITEM_NAME_PARAM_NAME to itemName)
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt
deleted file mode 100644
index e0baf86..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery.itemList
-
-import android.os.Bundle
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-
-private const val TITLE = "Operate List Main"
-
-object OperateListPageProvider : SettingsPageProvider {
-    override val name = "OpList"
-    private val owner = createSettingsPage()
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        return listOf(
-            ItemListPageProvider.buildInjectEntry("opPiP")!!.setLink(fromPage = owner).build(),
-            ItemListPageProvider.buildInjectEntry("opInstall")!!.setLink(fromPage = owner).build(),
-            ItemListPageProvider.buildInjectEntry("opDnD")!!.setLink(fromPage = owner).build(),
-            ItemListPageProvider.buildInjectEntry("opConnect")!!.setLink(fromPage = owner).build(),
-        )
-    }
-
-    override fun getTitle(arguments: Bundle?): String {
-        return TITLE
-    }
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
index f01ff38..9ad1c22 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
@@ -18,112 +18,69 @@
 
 import android.os.Bundle
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
 import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
+private const val TITLE = "Sample page with arguments"
+private const val STRING_PARAM_NAME = "stringParam"
+private const val INT_PARAM_NAME = "intParam"
+
 object ArgumentPageProvider : SettingsPageProvider {
-    // Defines all entry name in this page.
-    // Note that entry name would be used in log. DO NOT change it once it is set.
-    // One can still change the display name for better readability if necessary.
-    private enum class EntryEnum(val displayName: String) {
-        STRING_PARAM("string_param"),
-        INT_PARAM("int_param"),
-    }
+    override val name = "Argument"
 
-    private fun createEntry(owner: SettingsPage, entry: EntryEnum): SettingsEntryBuilder {
-        return SettingsEntryBuilder.create(owner, entry.name, entry.displayName)
-    }
-
-    override val name = SettingsPageProviderEnum.ARGUMENT.name
-    override val displayName = SettingsPageProviderEnum.ARGUMENT.displayName
-    override val parameter = ArgumentPageModel.parameter
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        if (!ArgumentPageModel.isValidArgument(arguments)) return emptyList()
-
-        val owner = createSettingsPage(arguments)
-        val entryList = mutableListOf<SettingsEntry>()
-        entryList.add(
-            createEntry(owner, EntryEnum.STRING_PARAM)
-                // Set attributes
-                .setIsSearchDataDynamic(true)
-                .setSearchDataFn { ArgumentPageModel.genStringParamSearchData() }
-                .setUiLayoutFn {
-                    // Set ui rendering
-                    Preference(ArgumentPageModel.create(it).genStringParamPreferenceModel())
-                }.build()
-        )
-
-        entryList.add(
-            createEntry(owner, EntryEnum.INT_PARAM)
-                // Set attributes
-                .setIsSearchDataDynamic(true)
-                .setSearchDataFn { ArgumentPageModel.genIntParamSearchData() }
-                .setUiLayoutFn {
-                    // Set ui rendering
-                    Preference(ArgumentPageModel.create(it).genIntParamPreferenceModel())
-                }.build()
-        )
-
-        entryList.add(buildInjectEntry("foo")!!.setLink(fromPage = owner).build())
-        entryList.add(buildInjectEntry("bar")!!.setLink(fromPage = owner).build())
-
-        return entryList
-    }
-
-    fun buildInjectEntry(stringParam: String): SettingsEntryBuilder? {
-        val arguments = ArgumentPageModel.buildArgument(stringParam)
-        if (!ArgumentPageModel.isValidArgument(arguments)) return null
-
-        return SettingsEntryBuilder.createInject(
-            owner = createSettingsPage(arguments),
-            label = "${name}_$stringParam",
-        )
-            .setSearchDataFn { ArgumentPageModel.genInjectSearchData() }
-            .setUiLayoutFn {
-                // Set ui rendering
-                Preference(ArgumentPageModel.create(it).genInjectPreferenceModel())
-            }
-    }
-
-    override fun getTitle(arguments: Bundle?): String {
-        return ArgumentPageModel.genPageTitle()
-    }
+    override val parameter = listOf(
+        navArgument(STRING_PARAM_NAME) { type = NavType.StringType },
+        navArgument(INT_PARAM_NAME) { type = NavType.IntType },
+    )
 
     @Composable
     override fun Page(arguments: Bundle?) {
-        val title = remember { getTitle(arguments) }
-        val entries = remember { buildEntry(arguments) }
-        val rtArgNext = remember { ArgumentPageModel.buildNextArgument(arguments) }
-        RegularScaffold(title) {
-            for (entry in entries) {
-                if (entry.toPage != null) {
-                    entry.UiLayout(rtArgNext)
-                } else {
-                    entry.UiLayout()
-                }
-            }
-        }
+        ArgumentPage(
+            stringParam = arguments!!.getString(STRING_PARAM_NAME, "default"),
+            intParam = arguments.getInt(INT_PARAM_NAME),
+        )
+    }
+
+    @Composable
+    fun EntryItem(stringParam: String, intParam: Int) {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val summary = { "$STRING_PARAM_NAME=$stringParam, $INT_PARAM_NAME=$intParam" }
+            override val onClick = navigator("$name/$stringParam/$intParam")
+        })
+    }
+}
+
+@Composable
+fun ArgumentPage(stringParam: String, intParam: Int) {
+    RegularScaffold(title = TITLE) {
+        Preference(object : PreferenceModel {
+            override val title = "String param value"
+            override val summary = { stringParam }
+        })
+
+        Preference(object : PreferenceModel {
+            override val title = "Int param value"
+            override val summary = { intParam.toString() }
+        })
+
+        ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = intParam + 1)
+
+        ArgumentPageProvider.EntryItem(stringParam = "bar", intParam = intParam + 1)
     }
 }
 
 @Preview(showBackground = true)
 @Composable
 private fun ArgumentPagePreview() {
-    SpaEnvironmentFactory.resetForPreview()
     SettingsTheme {
-        ArgumentPageProvider.Page(
-            ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0)
-        )
+        ArgumentPage(stringParam = "foo", intParam = 0)
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
deleted file mode 100644
index d763f77..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery.page
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavType
-import androidx.navigation.navArgument
-import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.PageModel
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.util.getIntArg
-import com.android.settingslib.spa.framework.util.getStringArg
-import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-
-private const val TAG = "ArgumentPageModel"
-
-// Defines all the resources for this page.
-// In real Settings App, resources data is defined in xml, rather than SPP.
-private const val PAGE_TITLE = "Sample page with arguments"
-private const val STRING_PARAM_TITLE = "String param value"
-private const val INT_PARAM_TITLE = "Int param value"
-private const val STRING_PARAM_NAME = "stringParam"
-private const val INT_PARAM_NAME = "rt_intParam"
-private val ARGUMENT_PAGE_KEYWORDS = listOf("argument keyword1", "argument keyword2")
-
-class ArgumentPageModel : PageModel() {
-
-    companion object {
-        val parameter = listOf(
-            navArgument(STRING_PARAM_NAME) { type = NavType.StringType },
-            navArgument(INT_PARAM_NAME) { type = NavType.IntType },
-        )
-
-        fun buildArgument(stringParam: String? = null, intParam: Int? = null): Bundle {
-            val args = Bundle()
-            if (stringParam != null) args.putString(STRING_PARAM_NAME, stringParam)
-            if (intParam != null) args.putInt(INT_PARAM_NAME, intParam)
-            return args
-        }
-
-        fun buildNextArgument(arguments: Bundle? = null): Bundle {
-            val intParam = parameter.getIntArg(INT_PARAM_NAME, arguments)
-            val nextIntParam = if (intParam != null) intParam + 1 else null
-            return buildArgument(intParam = nextIntParam)
-        }
-
-        fun isValidArgument(arguments: Bundle?): Boolean {
-            val stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments)
-            return (stringParam != null && listOf("foo", "bar").contains(stringParam))
-        }
-
-        fun genStringParamSearchData(): EntrySearchData {
-            return EntrySearchData(title = STRING_PARAM_TITLE)
-        }
-
-        fun genIntParamSearchData(): EntrySearchData {
-            return EntrySearchData(title = INT_PARAM_TITLE)
-        }
-
-        fun genInjectSearchData(): EntrySearchData {
-            return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS)
-        }
-
-        fun genPageTitle(): String {
-            return PAGE_TITLE
-        }
-
-        @Composable
-        fun create(arguments: Bundle?): ArgumentPageModel {
-            val pageModel: ArgumentPageModel = viewModel(key = arguments.toString())
-            pageModel.initOnce(arguments)
-            return pageModel
-        }
-    }
-
-    private var arguments: Bundle? = null
-    private var stringParam: String? = null
-    private var intParam: Int? = null
-
-    override fun initialize(arguments: Bundle?) {
-        SpaEnvironmentFactory.instance.logger.message(
-            TAG, "Initialize with args " + arguments.toString()
-        )
-        this.arguments = arguments
-        stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments)
-        intParam = parameter.getIntArg(INT_PARAM_NAME, arguments)
-    }
-
-    @Composable
-    fun genStringParamPreferenceModel(): PreferenceModel {
-        return object : PreferenceModel {
-            override val title = STRING_PARAM_TITLE
-            override val summary = { stringParam!! }
-        }
-    }
-
-    @Composable
-    fun genIntParamPreferenceModel(): PreferenceModel {
-        return object : PreferenceModel {
-            override val title = INT_PARAM_TITLE
-            override val summary = { intParam!!.toString() }
-        }
-    }
-
-    @Composable
-    fun genInjectPreferenceModel(): PreferenceModel {
-        val summaryArray = listOf(
-            "$STRING_PARAM_NAME=" + stringParam!!,
-            "$INT_PARAM_NAME=" + intParam!!
-        )
-        return object : PreferenceModel {
-            override val title = PAGE_TITLE
-            override val summary = { summaryArray.joinToString(", ") }
-            override val onClick = navigator(
-                SettingsPageProviderEnum.ARGUMENT.name + parameter.navLink(arguments)
-            )
-        }
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
index 345b47a..d31dab3 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
@@ -43,7 +43,7 @@
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
         entryList.add(
-            SettingsEntryBuilder.create( "Some Preference", owner)
+            SettingsEntryBuilder.create("Some Preference", owner)
                 .setSearchDataFn { EntrySearchData(title = "Some Preference") }
                 .setUiLayoutFn {
                     Preference(remember {
@@ -58,14 +58,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt
similarity index 86%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt
index ee22b96..021e84f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt
@@ -41,7 +41,7 @@
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
         entryList.add(
-            SettingsEntryBuilder.create( "Lottie Illustration", owner)
+            SettingsEntryBuilder.create("Lottie Illustration", owner)
                 .setUiLayoutFn {
                     Preference(object : PreferenceModel {
                         override val title = "Lottie Illustration"
@@ -54,7 +54,7 @@
                 }.build()
         )
         entryList.add(
-            SettingsEntryBuilder.create( "Image Illustration", owner)
+            SettingsEntryBuilder.create("Image Illustration", owner)
                 .setUiLayoutFn {
                     Preference(object : PreferenceModel {
                         override val title = "Image Illustration"
@@ -70,14 +70,12 @@
         return entryList
     }
 
-     fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt
index f1cbc37..4d47481 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt
@@ -30,9 +30,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -47,14 +45,12 @@
 object LoadingBarPageProvider : SettingsPageProvider {
     override val name = "LoadingBar"
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt
similarity index 88%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt
index 9026a24..47c49fe 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt
@@ -27,9 +27,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
@@ -46,14 +44,12 @@
 object ProgressBarPageProvider : SettingsPageProvider {
     override val name = "ProgressBar"
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt
index 89b10ee..572746b 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt
@@ -117,15 +117,14 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner).setUiLayoutFn {
-            Preference(
-                object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                }
-            )
-        }
+    @Composable
+    fun Entry() {
+        Preference(
+            object : PreferenceModel {
+                override val title = TITLE
+                override val onClick = navigator(name)
+            }
+        )
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt
index 603fcee..b83a026 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt
@@ -50,15 +50,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner).setUiLayoutFn {
-            Preference(
-                object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                }
-            )
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt
index d7de9b4..3bb526e 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt
@@ -23,9 +23,7 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.ListPreference
@@ -33,6 +31,8 @@
 import com.android.settingslib.spa.widget.preference.ListPreferenceOption
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.flow
@@ -41,30 +41,24 @@
 
 object ListPreferencePageProvider : SettingsPageProvider {
     override val name = "ListPreference"
-    private val owner = createSettingsPage()
 
-    override fun buildEntry(arguments: Bundle?) = listOf(
-        SettingsEntryBuilder.create("ListPreference", owner)
-            .setUiLayoutFn {
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(TITLE) {
+            Category {
                 SampleListPreference()
-            }.build(),
-        SettingsEntryBuilder.create("ListPreference not changeable", owner)
-            .setUiLayoutFn {
                 SampleNotChangeableListPreference()
-            }.build(),
-    )
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
             }
+        }
     }
 
-    override fun getTitle(arguments: Bundle?) = TITLE
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
+    }
 }
 
 @Composable
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt
similarity index 91%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt
index 0d85c0e3..f548160 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt
@@ -59,14 +59,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
index 1626b02..831b439 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
@@ -17,45 +17,44 @@
 package com.android.settingslib.spa.gallery.preference
 
 import android.os.Bundle
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
 
 private const val TITLE = "Category: Preference"
 
 object PreferenceMainPageProvider : SettingsPageProvider {
     override val name = "PreferenceMain"
-    private val owner = createSettingsPage()
 
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        return listOf(
-            PreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            SwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            MainSwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            ListPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            TwoTargetSwitchPreferencePageProvider.buildInjectEntry()
-                .setLink(fromPage = owner).build(),
-            ZeroStatePreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            IntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-            TopIntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
-        )
-    }
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(TITLE) {
+            Category {
+                PreferencePageProvider.Entry()
+                ListPreferencePageProvider.Entry()
             }
+            Category {
+                SwitchPreferencePageProvider.Entry()
+                MainSwitchPreferencePageProvider.Entry()
+                TwoTargetSwitchPreferencePageProvider.Entry()
+            }
+            Category {
+                ZeroStatePreferencePageProvider.Entry()
+                IntroPreferencePageProvider.Entry()
+                TopIntroPreferencePageProvider.Entry()
+            }
+        }
     }
 
-    override fun getTitle(arguments: Bundle?): String {
-        return TITLE
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt
deleted file mode 100644
index fc6f10f..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery.preference
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.android.settingslib.spa.framework.common.PageModel
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-private const val TAG = "PreferencePageModel"
-
-class PreferencePageModel : PageModel() {
-    companion object {
-        // Defines all the resources for this page.
-        // In real Settings App, resources data is defined in xml, rather than SPP.
-        const val PAGE_TITLE = "Sample Preference"
-        const val SIMPLE_PREFERENCE_TITLE = "Preference"
-        const val SIMPLE_PREFERENCE_SUMMARY = "Simple summary"
-        const val DISABLE_PREFERENCE_TITLE = "Disabled"
-        const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary"
-        const val ASYNC_PREFERENCE_TITLE = "Async Preference"
-        const val ASYNC_PREFERENCE_SUMMARY = "Async summary"
-        const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater"
-        const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater"
-        val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2")
-
-        @Composable
-        fun create(): PreferencePageModel {
-            val pageModel: PreferencePageModel = viewModel()
-            pageModel.initOnce()
-            return pageModel
-        }
-    }
-
-    private val spaLogger = SpaEnvironmentFactory.instance.logger
-
-    val asyncSummary = mutableStateOf("(loading)")
-    val asyncEnable = mutableStateOf(false)
-
-    private val manualUpdater = mutableStateOf(0)
-
-    private val autoUpdater = object : MutableLiveData<String>(" ") {
-        private var tick = 0
-        private var updateJob: Job? = null
-        override fun onActive() {
-            spaLogger.message(TAG, "autoUpdater.active")
-            updateJob = viewModelScope.launch(Dispatchers.IO) {
-                while (true) {
-                    delay(1000L)
-                    tick++
-                    spaLogger.message(TAG, "autoUpdater.value $tick")
-                    postValue(tick.toString())
-                }
-            }
-        }
-
-        override fun onInactive() {
-            spaLogger.message(TAG, "autoUpdater.inactive")
-            updateJob?.cancel()
-        }
-    }
-
-    override fun initialize(arguments: Bundle?) {
-        spaLogger.message(TAG, "initialize with args " + arguments.toString())
-        viewModelScope.launch(Dispatchers.IO) {
-            // Loading your data here.
-            delay(2000L)
-            asyncSummary.value = ASYNC_PREFERENCE_SUMMARY
-            asyncEnable.value = true
-        }
-    }
-
-    fun getManualUpdaterSummary(): State<String> {
-        spaLogger.message(TAG, "getManualUpdaterSummary")
-        return derivedStateOf { manualUpdater.value.toString() }
-    }
-
-    fun manualUpdaterOnClick() {
-        spaLogger.message(TAG, "manualUpdaterOnClick")
-        manualUpdater.value = manualUpdater.value + 1
-    }
-
-    fun getAutoUpdaterSummary(): LiveData<String> {
-        spaLogger.message(TAG, "getAutoUpdaterSummary")
-        return autoUpdater
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
index 6d1d346..f7649b9 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
@@ -18,187 +18,100 @@
 
 import android.os.Bundle
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Autorenew
 import androidx.compose.material.icons.outlined.DisabledByDefault
-import androidx.compose.material.icons.outlined.TouchApp
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.EntryStatusData
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
-import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_TITLE
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.MANUAL_UPDATE_PREFERENCE_TITLE
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.PAGE_TITLE
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
 import com.android.settingslib.spa.widget.ui.SettingsIcon
-
-private const val TAG = "PreferencePage"
+import kotlinx.coroutines.delay
 
 object PreferencePageProvider : SettingsPageProvider {
-    // Defines all entry name in this page.
-    // Note that entry name would be used in log. DO NOT change it once it is set.
-    // One can still change the display name for better readability if necessary.
-    enum class EntryEnum(val displayName: String) {
-        SIMPLE_PREFERENCE("preference"),
-        SUMMARY_PREFERENCE("preference_with_summary"),
-        SINGLE_LINE_SUMMARY_PREFERENCE("preference_with_single_line_summary"),
-        DISABLED_PREFERENCE("preference_disable"),
-        ASYNC_SUMMARY_PREFERENCE("preference_with_async_summary"),
-        MANUAL_UPDATE_PREFERENCE("preference_actionable"),
-        AUTO_UPDATE_PREFERENCE("preference_auto_update"),
-    }
 
-    override val name = SettingsPageProviderEnum.PREFERENCE.name
-    override val displayName = SettingsPageProviderEnum.PREFERENCE.displayName
-    private val spaLogger = SpaEnvironmentFactory.instance.logger
-    private val owner = createSettingsPage()
+    override val name = "Preference"
+    private const val PAGE_TITLE = "Sample Preference"
 
-    private fun createEntry(entry: EntryEnum): SettingsEntryBuilder {
-        return SettingsEntryBuilder.create(owner, entry.name, entry.displayName)
-    }
-
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        val entryList = mutableListOf<SettingsEntry>()
-        entryList.add(
-            createEntry(EntryEnum.SIMPLE_PREFERENCE)
-                .setMacro {
-                    spaLogger.message(TAG, "create macro for ${EntryEnum.SIMPLE_PREFERENCE}")
-                    SimplePreferenceMacro(title = SIMPLE_PREFERENCE_TITLE)
-                }
-                .setStatusDataFn { EntryStatusData(isDisabled = false) }
-                .build()
-        )
-        entryList.add(
-            createEntry(EntryEnum.SUMMARY_PREFERENCE)
-                .setMacro {
-                    spaLogger.message(TAG, "create macro for ${EntryEnum.SUMMARY_PREFERENCE}")
-                    SimplePreferenceMacro(
-                        title = SIMPLE_PREFERENCE_TITLE,
-                        summary = SIMPLE_PREFERENCE_SUMMARY,
-                        searchKeywords = SIMPLE_PREFERENCE_KEYWORDS,
-                    )
-                }
-                .setStatusDataFn { EntryStatusData(isDisabled = true) }
-                .build()
-        )
-        entryList.add(singleLineSummaryEntry())
-        entryList.add(
-            createEntry(EntryEnum.DISABLED_PREFERENCE)
-                .setHasMutableStatus(true)
-                .setMacro {
-                    spaLogger.message(TAG, "create macro for ${EntryEnum.DISABLED_PREFERENCE}")
-                    SimplePreferenceMacro(
-                        title = DISABLE_PREFERENCE_TITLE,
-                        summary = DISABLE_PREFERENCE_SUMMARY,
-                        disabled = true,
-                        icon = Icons.Outlined.DisabledByDefault,
-                    )
-                }
-                .setStatusDataFn { EntryStatusData(isDisabled = true) }
-                .build()
-        )
-        entryList.add(
-            createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE)
-                .setHasMutableStatus(true)
-                .setSearchDataFn {
-                    EntrySearchData(title = ASYNC_PREFERENCE_TITLE)
-                }
-                .setStatusDataFn { EntryStatusData(isDisabled = false) }
-                .setUiLayoutFn {
-                    val model = PreferencePageModel.create()
-                    Preference(
-                        object : PreferenceModel {
-                            override val title = ASYNC_PREFERENCE_TITLE
-                            override val summary = { model.asyncSummary.value }
-                            override val enabled = { model.asyncEnable.value }
-                        }
-                    )
-                }.build()
-        )
-        entryList.add(
-            createEntry(EntryEnum.MANUAL_UPDATE_PREFERENCE)
-                .setUiLayoutFn {
-                    val model = PreferencePageModel.create()
-                    val manualUpdaterSummary = remember { model.getManualUpdaterSummary() }
-                    Preference(
-                        object : PreferenceModel {
-                            override val title = MANUAL_UPDATE_PREFERENCE_TITLE
-                            override val summary = { manualUpdaterSummary.value }
-                            override val onClick = { model.manualUpdaterOnClick() }
-                            override val icon = @Composable {
-                                SettingsIcon(imageVector = Icons.Outlined.TouchApp)
-                            }
-                        }
-                    )
-                }.build()
-        )
-        entryList.add(
-            createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE)
-                .setUiLayoutFn {
-                    val model = PreferencePageModel.create()
-                    val autoUpdaterSummary = remember {
-                        model.getAutoUpdaterSummary()
-                    }.observeAsState(" ")
-                    Preference(
-                        object : PreferenceModel {
-                            override val title = AUTO_UPDATE_PREFERENCE_TITLE
-                            override val summary = { autoUpdaterSummary.value }
-                            override val icon = @Composable {
-                                SettingsIcon(imageVector = Icons.Outlined.Autorenew)
-                            }
-                        }
-                    )
-                }.build()
-        )
-
-        return entryList
-    }
-
-    private fun singleLineSummaryEntry() = createEntry(EntryEnum.SINGLE_LINE_SUMMARY_PREFERENCE)
-        .setUiLayoutFn {
-            val summary = stringResource(R.string.single_line_summary_preference_summary)
-            Preference(
-                model = object : PreferenceModel {
-                    override val title: String =
-                        stringResource(R.string.single_line_summary_preference_title)
-                    override val summary = { summary }
-                },
-                singleLineSummary = true,
-            )
-        }
-        .build()
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = owner)
-            .setMacro {
-                spaLogger.message(TAG, "create macro for INJECT entry")
-                SimplePreferenceMacro(
-                    title = PAGE_TITLE,
-                    clickRoute = SettingsPageProviderEnum.PREFERENCE.name
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(PAGE_TITLE) {
+            Category {
+                Preference(object : PreferenceModel {
+                    override val title = "Preference"
+                })
+                Preference(object : PreferenceModel {
+                    override val title = "Preference"
+                    override val summary = { "Simple summary" }
+                })
+                val summary = stringResource(R.string.single_line_summary_preference_summary)
+                Preference(
+                    model = object : PreferenceModel {
+                        override val title =
+                            stringResource(R.string.single_line_summary_preference_title)
+                        override val summary = { summary }
+                    },
+                    singleLineSummary = true,
                 )
             }
+            Category {
+                Preference(object : PreferenceModel {
+                    override val title = "Disabled"
+                    override val summary = { "Disabled summary" }
+                    override val enabled = { false }
+                    override val icon = @Composable {
+                        SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
+                    }
+                })
+            }
+            Category {
+                Preference(object : PreferenceModel {
+                    override val title = "Preference"
+                    val asyncSummary by produceState(initialValue = " ") {
+                        delay(1000L)
+                        value = "Async summary"
+                    }
+                    override val summary = { asyncSummary }
+                })
+
+                var count by remember { mutableIntStateOf(0) }
+                Preference(object : PreferenceModel {
+                    override val title = "Click me"
+                    override val summary = { count.toString() }
+                    override val onClick: (() -> Unit) = { count++ }
+                })
+
+                var ticks by remember { mutableIntStateOf(0) }
+                LaunchedEffect(ticks) {
+                    delay(1000L)
+                    ticks++
+                }
+                Preference(object : PreferenceModel {
+                    override val title = "Ticker"
+                    override val summary = { ticks.toString() }
+                })
+            }
+        }
     }
 
-    override fun getTitle(arguments: Bundle?): String {
-        return PAGE_TITLE
+    @Composable
+    fun Entry() {
+        Preference(model = object : PreferenceModel {
+            override val title = PAGE_TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt
index f2225fa..9508d50 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt
@@ -27,16 +27,15 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
 import com.android.settingslib.spa.widget.ui.SettingsIcon
 import kotlinx.coroutines.delay
 
@@ -44,56 +43,26 @@
 
 object SwitchPreferencePageProvider : SettingsPageProvider {
     override val name = "SwitchPreference"
-    private val owner = createSettingsPage()
 
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        val entryList = mutableListOf<SettingsEntry>()
-        entryList.add(
-            SettingsEntryBuilder.create( "SwitchPreference", owner)
-                .setUiLayoutFn {
-                    SampleSwitchPreference()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "SwitchPreference with summary", owner)
-                .setUiLayoutFn {
-                    SampleSwitchPreferenceWithSummary()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "SwitchPreference with async summary", owner)
-                .setUiLayoutFn {
-                    SampleSwitchPreferenceWithAsyncSummary()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "SwitchPreference not changeable", owner)
-                .setUiLayoutFn {
-                    SampleNotChangeableSwitchPreference()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "SwitchPreference with icon", owner)
-                .setUiLayoutFn {
-                    SampleSwitchPreferenceWithIcon()
-                }.build()
-        )
-
-        return entryList
-    }
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(TITLE) {
+            Category {
+                SampleSwitchPreference()
+                SampleSwitchPreferenceWithSummary()
+                SampleSwitchPreferenceWithAsyncSummary()
+                SampleNotChangeableSwitchPreference()
+                SampleSwitchPreferenceWithIcon()
             }
+        }
     }
 
-    override fun getTitle(arguments: Bundle?): String {
-        return TITLE
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt
index b251266..ee08e30 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt
@@ -50,15 +50,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner).setUiLayoutFn {
-            Preference(
-                object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                }
-            )
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt
index 19de31d..1a89bb2 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt
@@ -25,66 +25,40 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
 import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
 import kotlinx.coroutines.delay
 
 private const val TITLE = "Sample TwoTargetSwitchPreference"
 
 object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider {
     override val name = "TwoTargetSwitchPreference"
-    private val owner = createSettingsPage()
 
-    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        val entryList = mutableListOf<SettingsEntry>()
-        entryList.add(
-            SettingsEntryBuilder.create( "TwoTargetSwitchPreference", owner)
-                .setUiLayoutFn {
-                    SampleTwoTargetSwitchPreference()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "TwoTargetSwitchPreference with summary", owner)
-                .setUiLayoutFn {
-                    SampleTwoTargetSwitchPreferenceWithSummary()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "TwoTargetSwitchPreference with async summary", owner)
-                .setUiLayoutFn {
-                    SampleTwoTargetSwitchPreferenceWithAsyncSummary()
-                }.build()
-        )
-        entryList.add(
-            SettingsEntryBuilder.create( "TwoTargetSwitchPreference not changeable", owner)
-                .setUiLayoutFn {
-                    SampleNotChangeableTwoTargetSwitchPreference()
-                }.build()
-        )
-
-        return entryList
-    }
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(TITLE) {
+            Category {
+                SampleTwoTargetSwitchPreference()
+                SampleTwoTargetSwitchPreferenceWithSummary()
+                SampleTwoTargetSwitchPreferenceWithAsyncSummary()
+                SampleNotChangeableTwoTargetSwitchPreference()
             }
+        }
     }
 
-    override fun getTitle(arguments: Bundle?): String {
-        return TITLE
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt
index 4a9c5c8..04b5ceb 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt
@@ -53,14 +53,12 @@
         return entryList
     }
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt
index 66cc38f..c9a6557 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt
@@ -17,7 +17,7 @@
 package com.android.settingslib.spa.gallery.scaffold
 
 import android.os.Bundle
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
@@ -34,13 +34,13 @@
         ScrollablePagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
     )
 
-    fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
-        .setUiLayoutFn {
-            Preference(object : PreferenceModel {
-                override val title = TITLE
-                override val onClick = navigator(name)
-            })
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
+    }
 
     override fun getTitle(arguments: Bundle?) = TITLE
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
index eac06e3..0d7cad10 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
@@ -18,9 +18,7 @@
 
 import android.os.Bundle
 import androidx.compose.runtime.Composable
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -32,15 +30,13 @@
 object SearchScaffoldPageProvider : SettingsPageProvider {
     override val name = "SearchScaffold"
 
-    private val owner = createSettingsPage()
-
-    fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
-        .setUiLayoutFn {
-            Preference(object : PreferenceModel {
-                override val title = TITLE
-                override val onClick = navigator(name)
-            })
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
+    }
 
     @Composable
     override fun Page(arguments: Bundle?) {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
index a0ab2ce..7b02fcb 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
@@ -27,9 +27,7 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.gallery.R
@@ -49,15 +47,13 @@
 object SuwScaffoldPageProvider : SettingsPageProvider {
     override val name = "SuwScaffold"
 
-    private val owner = createSettingsPage()
-
-    fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
-        .setUiLayoutFn {
-            Preference(object : PreferenceModel {
-                override val title = TITLE
-                override val onClick = navigator(name)
-            })
-        }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
+    }
 
     @Composable
     override fun Page(arguments: Bundle?) {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
similarity index 81%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
index 7a1fad0..4d3a78a5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
@@ -19,7 +19,6 @@
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.EntrySearchData
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -31,7 +30,6 @@
 import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import com.android.settingslib.spa.widget.ui.Category
-import com.android.settingslib.spa.widget.ui.CategoryTitle
 
 private const val TITLE = "Sample Category"
 
@@ -39,15 +37,14 @@
     override val name = "Category"
     private val owner = createSettingsPage()
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
+    @Composable
+    fun Entry() {
+        Preference(
+            object : PreferenceModel {
+                override val title = TITLE
+                override val onClick = navigator(name)
             }
-            .setSearchDataFn { EntrySearchData(title = TITLE) }
+        )
     }
 
     override fun getTitle(arguments: Bundle?): String {
@@ -70,7 +67,6 @@
             SettingsEntryBuilder.create("Preference 3", owner)
                 .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") }
                 .build()
-
         )
         entryList.add(
             SettingsEntryBuilder.create("Preference 4", owner)
@@ -84,11 +80,11 @@
     override fun Page(arguments: Bundle?) {
         val entries = buildEntry(arguments)
         RegularScaffold(title = getTitle(arguments)) {
-            CategoryTitle("Category A")
-            entries[0].UiLayout()
-            entries[1].UiLayout()
-
-            Category("Category B") {
+            Category("Category A") {
+                entries[0].UiLayout()
+                entries[1].UiLayout()
+            }
+            Category {
                 entries[2].UiLayout()
                 entries[3].UiLayout()
             }
@@ -99,7 +95,5 @@
 @Preview(showBackground = true)
 @Composable
 private fun SpinnerPagePreview() {
-    SettingsTheme {
-        SpinnerPageProvider.Page(null)
-    }
+    SettingsTheme { CategoryPageProvider.Page(null) }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
index f897d8c..e919129 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
@@ -21,10 +21,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.widget.preference.Preference
@@ -37,17 +34,12 @@
 object CopyablePageProvider : SettingsPageProvider {
     override val name = "Copyable"
 
-    private val owner = createSettingsPage()
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner)
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
-            .setSearchDataFn { EntrySearchData(title = TITLE) }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     @Composable
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt
index 5c5c504..7a4b632 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt
@@ -23,9 +23,7 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
@@ -39,14 +37,12 @@
 object SpinnerPageProvider : SettingsPageProvider {
     override val name = "Spinner"
 
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
-            .setUiLayoutFn {
-                Preference(object : PreferenceModel {
-                    override val title = TITLE
-                    override val onClick = navigator(name)
-                })
-            }
+    @Composable
+    fun Entry() {
+        Preference(object : PreferenceModel {
+            override val title = TITLE
+            override val onClick = navigator(name)
+        })
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index f8c791a..ab95162 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -24,6 +24,7 @@
     val paddingExtraSmall = 4.dp
     val paddingSmall = if (isSpaExpressiveEnabled) 8.dp else 4.dp
     val paddingExtraSmall5 = 10.dp
+    val paddingExtraSmall6 = 12.dp
     val paddingLarge = 16.dp
     val paddingExtraLarge = 24.dp
 
@@ -36,9 +37,9 @@
 
     val itemIconSize = 24.dp
     val itemIconContainerSize = 72.dp
-    val itemPaddingStart = paddingExtraLarge
+    val itemPaddingStart = if (isSpaExpressiveEnabled) paddingLarge else paddingExtraLarge
     val itemPaddingEnd = paddingLarge
-    val itemPaddingVertical = paddingLarge
+    val itemPaddingVertical = if (isSpaExpressiveEnabled) paddingExtraSmall6 else paddingLarge
     val itemPadding = PaddingValues(
         start = itemPaddingStart,
         top = itemPaddingVertical,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
index f7c5414..c787715 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
@@ -24,5 +24,7 @@
 
     val CornerMedium = RoundedCornerShape(12.dp)
 
+    val categoryCorner = RoundedCornerShape(20.dp)
+
     val CornerExtraLarge = RoundedCornerShape(28.dp)
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt
index 185fd29..38707b0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt
@@ -57,6 +57,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraSmall
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 import com.android.settingslib.spa.widget.ui.SettingsBody
 import com.android.settingslib.spa.widget.ui.SettingsTitle
 
@@ -159,7 +160,9 @@
 @Composable
 fun BannerTitleHeader(title: String, onDismiss: (() -> Unit)? = null) {
     Row(Modifier.fillMaxWidth()) {
-        Box(modifier = Modifier.weight(1f)) { SettingsTitle(title) }
+        Box(modifier = Modifier.weight(1f)) {
+            Text(text = title, style = MaterialTheme.typography.titleMedium.toSemiBoldWeight())
+        }
         Spacer(modifier = Modifier.padding(SettingsDimension.paddingSmall))
         DismissButton(onDismiss)
     }
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 5bb57b8..203a8bd 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
@@ -56,6 +56,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.theme.divider
 import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 
 data class ActionButton(
     val text: String,
@@ -129,7 +130,7 @@
                 Text(
                     text = actionButton.text,
                     textAlign = TextAlign.Center,
-                    style = MaterialTheme.typography.labelMedium,
+                    style = MaterialTheme.typography.labelLarge.toSemiBoldWeight(),
                 )
             }
         }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 265864e..490936f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
@@ -99,7 +100,16 @@
             dismissButton?.let {
                 { if (isSpaExpressiveEnabled) DismissButton(it) else Button(it) }
             },
-        title = title?.let { { CenterRow { Text(it) } } },
+        title =
+            title?.let {
+                {
+                    CenterRow {
+                        if (isSpaExpressiveEnabled)
+                            Text(it, style = MaterialTheme.typography.bodyLarge)
+                        else Text(it)
+                    }
+                }
+            },
         text =
             text?.let {
                 { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index 23a8e78..c68ec78 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.spa.widget.preference
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -25,16 +26,20 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled
+import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
 import com.android.settingslib.spa.widget.ui.SettingsTitle
 
 @Composable
@@ -51,10 +56,17 @@
     widget: @Composable () -> Unit = {},
 ) {
     Row(
-        modifier = modifier
-            .fillMaxWidth()
-            .semantics(mergeDescendants = true) {}
-            .padding(end = paddingEnd),
+        modifier =
+            modifier
+                .fillMaxWidth()
+                .semantics(mergeDescendants = true) {}
+                .then(
+                    if (isSpaExpressiveEnabled)
+                        Modifier.clip(SettingsShape.CornerExtraSmall)
+                            .background(MaterialTheme.colorScheme.surfaceBright)
+                    else Modifier
+                )
+                .padding(end = paddingEnd),
         verticalAlignment = Alignment.CenterVertically,
     ) {
         val alphaModifier = Modifier.alphaForEnabled(enabled())
@@ -63,20 +75,14 @@
             title = title,
             titleContentDescription = titleContentDescription,
             subTitle = subTitle,
-            modifier = alphaModifier
-                .weight(1f)
-                .padding(vertical = paddingVertical),
+            modifier = alphaModifier.weight(1f).padding(vertical = paddingVertical),
         )
         widget()
     }
 }
 
 @Composable
-internal fun BaseIcon(
-    icon: @Composable (() -> Unit)?,
-    modifier: Modifier,
-    paddingStart: Dp,
-) {
+internal fun BaseIcon(icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp) {
     if (icon != null) {
         Box(
             modifier = modifier.size(SettingsDimension.itemIconContainerSize),
@@ -107,11 +113,6 @@
 @Composable
 private fun BaseLayoutPreview() {
     SettingsTheme {
-        BaseLayout(
-            title = "Title",
-            subTitle = {
-                HorizontalDivider(thickness = 10.dp)
-            }
-        )
+        BaseLayout(title = "Title", subTitle = { HorizontalDivider(thickness = 10.dp) })
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt
index 22a5755..7707376 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt
@@ -36,6 +36,7 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 
 @Composable
 fun IntroPreference(
@@ -112,7 +113,7 @@
         Text(
             text = title,
             textAlign = TextAlign.Center,
-            style = MaterialTheme.typography.titleLarge,
+            style = MaterialTheme.typography.titleLarge.toSemiBoldWeight(),
             color = MaterialTheme.colorScheme.onSurface,
         )
     }
@@ -126,7 +127,7 @@
             Text(
                 text = description,
                 textAlign = TextAlign.Center,
-                style = MaterialTheme.typography.titleMedium,
+                style = MaterialTheme.typography.bodyLarge,
                 color = MaterialTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.padding(top = SettingsDimension.paddingExtraSmall),
             )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
index 3f2e772..b771f36 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
@@ -47,6 +47,7 @@
 import androidx.graphics.shapes.RoundedPolygon
 import androidx.graphics.shapes.star
 import androidx.graphics.shapes.toPath
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
 
 @Composable
 fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: String? = null) {
@@ -80,7 +81,7 @@
             Text(
                 text = text,
                 textAlign = TextAlign.Center,
-                style = MaterialTheme.typography.titleMedium,
+                style = MaterialTheme.typography.titleMedium.toSemiBoldWeight(),
                 color = MaterialTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.padding(top = 24.dp),
             )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index 48cd145..6c5581f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -16,9 +16,13 @@
 
 package com.android.settingslib.spa.widget.ui
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.TouchApp
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -27,25 +31,31 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
 
-/**
- * A category title that is placed before a group of similar items.
- */
+/** A category title that is placed before a group of similar items. */
 @Composable
 fun CategoryTitle(title: String) {
     Text(
         text = title,
-        modifier = Modifier.padding(
-            start = SettingsDimension.itemPaddingStart,
-            top = 20.dp,
-            end = SettingsDimension.itemPaddingEnd,
-            bottom = 8.dp,
-        ),
+        modifier =
+            Modifier.padding(
+                start = SettingsDimension.itemPaddingStart,
+                top = 20.dp,
+                end =
+                    if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall
+                    else SettingsDimension.itemPaddingEnd,
+                bottom = 8.dp,
+            ),
         color = MaterialTheme.colorScheme.primary,
         style = MaterialTheme.typography.labelMedium,
     )
@@ -56,14 +66,31 @@
  * visually separates groups of items.
  */
 @Composable
-fun Category(title: String, content: @Composable ColumnScope.() -> Unit) {
-    Column {
+fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) {
+    Column(
+        modifier =
+            if (isSpaExpressiveEnabled)
+                Modifier.padding(
+                    horizontal = SettingsDimension.paddingLarge,
+                    vertical = SettingsDimension.paddingSmall,
+                )
+            else Modifier
+    ) {
         var displayTitle by remember { mutableStateOf(false) }
-        if (displayTitle) CategoryTitle(title = title)
+        if (title != null && displayTitle) CategoryTitle(title = title)
         Column(
-            modifier = Modifier.onGloballyPositioned { coordinates ->
-                displayTitle = coordinates.size.height > 0
-            },
+            modifier =
+                Modifier.onGloballyPositioned { coordinates ->
+                        displayTitle = coordinates.size.height > 0
+                    }
+                    .then(
+                        if (isSpaExpressiveEnabled)
+                            Modifier.fillMaxWidth().clip(SettingsShape.categoryCorner)
+                        else Modifier
+                    ),
+            verticalArrangement =
+                if (isSpaExpressiveEnabled) Arrangement.spacedBy(SettingsDimension.paddingTiny)
+                else Arrangement.Top,
             content = content,
         )
     }
@@ -73,6 +100,21 @@
 @Composable
 private fun CategoryPreview() {
     SettingsTheme {
-        CategoryTitle("Appearance")
+        Category("Appearance") {
+            Preference(
+                object : PreferenceModel {
+                    override val title = "Title"
+                    override val summary = { "Summary" }
+                }
+            )
+            Preference(
+                object : PreferenceModel {
+                    override val title = "Title"
+                    override val summary = { "Summary" }
+                    override val icon =
+                        @Composable { SettingsIcon(imageVector = Icons.Outlined.TouchApp) }
+                }
+            )
+        }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 616ab07..612c193 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -709,6 +709,9 @@
     @WorkerThread
     public static boolean hasConnectedBroadcastSourceForBtDevice(
             @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) {
+        if (Flags.audioSharingHysteresisModeFix()) {
+            return hasActiveLocalBroadcastSourceForBtDevice(device, localBtManager);
+        }
         LocalBluetoothLeBroadcastAssistant assistant =
                 localBtManager == null
                         ? null
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index a3f9e51..364e95c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -52,6 +52,7 @@
 import androidx.annotation.RequiresApi;
 
 import com.android.settingslib.R;
+import com.android.settingslib.flags.Flags;
 
 import com.google.common.collect.ImmutableList;
 
@@ -1134,20 +1135,8 @@
             Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null");
             return;
         }
-        List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices();
-        List<BluetoothDevice> devicesInSharing =
-                connectedDevices.stream()
-                        .filter(
-                                bluetoothDevice -> {
-                                    List<BluetoothLeBroadcastReceiveState> sourceList =
-                                            mServiceBroadcastAssistant.getAllSources(
-                                                    bluetoothDevice);
-                                    return !sourceList.isEmpty()
-                                            && sourceList.stream()
-                                                    .anyMatch(BluetoothUtils::isConnected);
-                                })
-                        .collect(Collectors.toList());
-        if (devicesInSharing.isEmpty()) {
+        List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast();
+        if (devicesInBroadcast.isEmpty()) {
             Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast");
             return;
         }
@@ -1156,7 +1145,7 @@
         BluetoothDevice targetDevice = null;
         // Find the earliest connected device in sharing session.
         int targetDeviceIdx = -1;
-        for (BluetoothDevice device : devicesInSharing) {
+        for (BluetoothDevice device : devicesInBroadcast) {
             if (devices.contains(device)) {
                 int idx = devices.indexOf(device);
                 if (idx > targetDeviceIdx) {
@@ -1169,10 +1158,6 @@
             Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null");
             return;
         }
-        Log.d(
-                TAG,
-                "updateFallbackActiveDeviceIfNeeded, set active device: "
-                        + targetDevice.getAnonymizedAddress());
         CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice);
         if (targetCachedDevice == null) {
             Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device");
@@ -1180,16 +1165,37 @@
         }
         int fallbackActiveGroupId = getFallbackActiveGroupId();
         if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
-                && getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
+                && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
             Log.d(
                     TAG,
                     "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: "
                             + fallbackActiveGroupId);
             return;
         }
+        Log.d(
+                TAG,
+                "updateFallbackActiveDeviceIfNeeded, set active device: "
+                        + targetDevice.getAnonymizedAddress());
         targetCachedDevice.setActive();
     }
 
+    private List<BluetoothDevice> getDevicesInBroadcast() {
+        boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix();
+        List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices();
+        return connectedDevices.stream()
+                .filter(
+                        bluetoothDevice -> {
+                            List<BluetoothLeBroadcastReceiveState> sourceList =
+                                    mServiceBroadcastAssistant.getAllSources(
+                                            bluetoothDevice);
+                            return !sourceList.isEmpty() && sourceList.stream().anyMatch(
+                                    source -> hysteresisModeFixEnabled
+                                            ? BluetoothUtils.isSourceMatched(source, mBroadcastId)
+                                            : BluetoothUtils.isConnected(source));
+                        })
+                .collect(Collectors.toList());
+    }
+
     private int getFallbackActiveGroupId() {
         return Settings.Secure.getInt(
                 mContext.getContentResolver(),
@@ -1197,23 +1203,6 @@
                 BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
     }
 
-    private int getGroupId(CachedBluetoothDevice cachedDevice) {
-        int groupId = cachedDevice.getGroupId();
-        String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
-        if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
-            Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
-            return groupId;
-        }
-        for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
-            if (profile instanceof LeAudioProfile) {
-                Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
-                return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
-            }
-        }
-        Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
-        return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
-    }
-
     private void notifyBroadcastStateChange(@BroadcastState int state) {
         if (!mContext.getPackageName().equals(SETTINGS_PKG)) {
             Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings.");
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
index 5656f38..3627669 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
@@ -63,7 +63,8 @@
                                 },
                             moreSettingsHelpItem = readParcelable(
                                 DeviceSettingItem::class.java.classLoader
-                            )
+                            ),
+                            extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
                         )
                     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 8eedb35..0e060df 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -47,6 +47,7 @@
 import android.util.Pair;
 
 import com.android.internal.R;
+import com.android.settingslib.flags.Flags;
 import com.android.settingslib.widget.AdaptiveIcon;
 
 import com.google.common.collect.ImmutableList;
@@ -605,6 +606,7 @@
 
     @Test
     public void testHasConnectedBroadcastSource_leadDeviceConnectedToBroadcastSource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
         BluetoothDevice memberDevice = mock(BluetoothDevice.class);
@@ -630,6 +632,7 @@
 
     @Test
     public void testHasConnectedBroadcastSource_memberDeviceConnectedToBroadcastSource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
         BluetoothDevice memberDevice = mock(BluetoothDevice.class);
@@ -655,6 +658,7 @@
 
     @Test
     public void testHasConnectedBroadcastSource_deviceNotConnectedToBroadcastSource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
 
         List<Long> bisSyncState = new ArrayList<>();
@@ -672,6 +676,7 @@
 
     @Test
     public void testHasConnectedBroadcastSourceForBtDevice_deviceConnectedToBroadcastSource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
         List<Long> bisSyncState = new ArrayList<>();
         bisSyncState.add(1L);
         when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
@@ -688,6 +693,7 @@
 
     @Test
     public void testHasConnectedBroadcastSourceForBtDevice_deviceNotConnectedToBroadcastSource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
         List<Long> bisSyncState = new ArrayList<>();
         when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 
@@ -702,6 +708,106 @@
     }
 
     @Test
+    public void hasConnectedBroadcastSource_hysteresisFix_leadDeviceHasActiveLocalSource() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
+        BluetoothDevice memberDevice = mock(BluetoothDevice.class);
+        when(memberCachedDevice.getDevice()).thenReturn(memberDevice);
+        Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>();
+        memberCachedDevices.add(memberCachedDevice);
+        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices);
+
+
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+        when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+        when(mAssistant.getAllSources(memberDevice)).thenReturn(Collections.emptyList());
+
+        assertThat(
+                BluetoothUtils.hasConnectedBroadcastSource(
+                        mCachedBluetoothDevice, mLocalBluetoothManager))
+                .isTrue();
+    }
+
+    @Test
+    public void hasConnectedBroadcastSource_hysteresisFix_memberDeviceHasActiveLocalSource() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
+        BluetoothDevice memberDevice = mock(BluetoothDevice.class);
+        when(memberCachedDevice.getDevice()).thenReturn(memberDevice);
+        Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>();
+        memberCachedDevices.add(memberCachedDevice);
+        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices);
+
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+        when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(memberDevice)).thenReturn(sourceList);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(Collections.emptyList());
+
+        assertThat(
+                BluetoothUtils.hasConnectedBroadcastSource(
+                        mCachedBluetoothDevice, mLocalBluetoothManager))
+                .isTrue();
+    }
+
+    @Test
+    public void hasConnectedBroadcastSource_hysteresisFix_deviceNoActiveLocalSource() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                BluetoothUtils.hasConnectedBroadcastSource(
+                        mCachedBluetoothDevice, mLocalBluetoothManager))
+                .isFalse();
+    }
+
+    @Test
+    public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceHasActiveLocalSource() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+        when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(
+                        mBluetoothDevice, mLocalBluetoothManager))
+                .isTrue();
+    }
+
+    @Test
+    public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceNoActiveLocalSource() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID);
+        when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(
+                        mBluetoothDevice, mLocalBluetoothManager))
+                .isFalse();
+    }
+
+    @Test
     public void testHasActiveLocalBroadcastSourceForBtDevice_hasActiveLocalSource() {
         when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID);
         when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
index 7f17293..ebaad34 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
@@ -86,6 +86,7 @@
         assertThat(fromParcel.moreSettingsHelpItem?.packageName).isEqualTo("package_name_2")
         assertThat(fromParcel.moreSettingsHelpItem?.className).isEqualTo("class_name_2")
         assertThat(fromParcel.moreSettingsHelpItem?.intentAction).isEqualTo("intent_action_2")
+        assertThat(fromParcel.extras.getString("key1")).isEqualTo("value1")
     }
 
     private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java
deleted file mode 100644
index 6c8fd50..0000000
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.settingslib.widget;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.view.View;
-
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settingslib.widget.preference.app.R;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-@RunWith(RobolectricTestRunner.class)
-public class AppPreferenceTest {
-
-    private Context mContext;
-    private View mRootView;
-    private AppPreference mPref;
-    private PreferenceViewHolder mHolder;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mRootView = View.inflate(mContext, R.layout.preference_app, null /* parent */);
-        mHolder = PreferenceViewHolder.createInstanceForTests(mRootView);
-        mPref = new AppPreference(mContext);
-    }
-
-    @Test
-    public void setProgress_showProgress() {
-        mPref.setProgress(1);
-        mPref.onBindViewHolder(mHolder);
-
-        assertThat(mHolder.findViewById(android.R.id.progress).getVisibility())
-                .isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void foobar_testName() {
-        float iconSize = mContext.getResources().getDimension(com.android.settingslib.widget.theme.R.dimen.secondary_app_icon_size);
-        assertThat(Float.floatToIntBits(iconSize)).isEqualTo(Float.floatToIntBits(32));
-    }
-}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 88cc152..cb1411b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -559,6 +559,13 @@
 }
 
 flag {
+    name: "volume_redesign"
+    namespace: "systemui"
+    description: "Enables Volume BC25 visuals update"
+    bug: "368308908"
+}
+
+flag {
     name: "clipboard_shared_transitions"
     namespace: "systemui"
     description: "Show shared transitions from clipboard"
@@ -1118,6 +1125,16 @@
 }
 
 flag {
+  name: "media_controls_umo_inflation_in_background"
+  namespace: "systemui"
+  description: "Inflate UMO in background thread"
+  bug: "368514198"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   namespace: "systemui"
   name: "enable_view_capture_tracing"
   description: "Enables view capture tracing in System UI."
@@ -1421,4 +1438,4 @@
    metadata {
        purpose: PURPOSE_BUGFIX
    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index f4d1242..bcd3337 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -274,7 +274,7 @@
                                     if (layoutDirection == LayoutDirection.Rtl)
                                         screenWidth - offset.x
                                     else offset.x,
-                                    offset.y
+                                    offset.y,
                                 ) - contentOffset
                             val index = firstIndexAtOffset(gridState, adjustedOffset)
                             val key =
@@ -310,6 +310,9 @@
                                         it.changedToUp() || it.changedToUpIgnoreConsumed()
                                     }
                                 )
+
+                                // Reset state once touch ends.
+                                viewModel.onResetTouchState()
                             }
                         }
                     }
@@ -330,7 +333,7 @@
                                         if (layoutDirection == LayoutDirection.Rtl)
                                             screenWidth - offset.x
                                         else offset.x,
-                                        offset.y
+                                        offset.y,
                                     ) - it.positionInWindow() - contentOffset
                                 }
                             val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
@@ -344,14 +347,11 @@
                             }
                         }
                     }
-                },
+                }
     ) {
         AccessibilityContainer(viewModel) {
             if (!viewModel.isEditMode && isEmptyState) {
-                EmptyStateCta(
-                    contentPadding = contentPadding,
-                    viewModel = viewModel,
-                )
+                EmptyStateCta(contentPadding = contentPadding, viewModel = viewModel)
             } else {
                 val slideOffsetInPx =
                     with(LocalDensity.current) { Dimensions.SlideOffsetY.toPx().toInt() }
@@ -364,7 +364,7 @@
                         ) +
                             slideInVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                initialOffsetY = { -slideOffsetInPx }
+                                initialOffsetY = { -slideOffsetInPx },
                             ),
                     exit =
                         fadeOut(
@@ -372,7 +372,7 @@
                         ) +
                             slideOutVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                targetOffsetY = { -slideOffsetInPx }
+                                targetOffsetY = { -slideOffsetInPx },
                             ),
                     modifier = Modifier.fillMaxSize(),
                 ) {
@@ -389,7 +389,7 @@
                                     removeEnabled = removeButtonEnabled,
                                     offset =
                                         gridCoordinates?.let { it.positionInWindow() + offset },
-                                    containerToCheck = removeButtonCoordinates
+                                    containerToCheck = removeButtonCoordinates,
                                 )
                             },
                             gridState = gridState,
@@ -410,7 +410,7 @@
                 enter =
                     fadeIn(animationSpec = tween(durationMillis = 250, easing = LinearEasing)) +
                         slideInVertically(
-                            animationSpec = tween(durationMillis = 1000, easing = Emphasized),
+                            animationSpec = tween(durationMillis = 1000, easing = Emphasized)
                         ),
                 exit =
                     fadeOut(animationSpec = tween(durationMillis = 167, easing = LinearEasing)) +
@@ -434,7 +434,7 @@
                             viewModel.setSelectedKey(null)
                         }
                     },
-                    removeEnabled = removeButtonEnabled
+                    removeEnabled = removeButtonEnabled,
                 )
             }
         }
@@ -451,7 +451,7 @@
                 title = stringResource(id = R.string.dialog_title_to_allow_any_widget),
                 positiveButtonText = stringResource(id = R.string.button_text_to_open_settings),
                 onConfirm = viewModel::onEnableWidgetDialogConfirm,
-                onCancel = viewModel::onEnableWidgetDialogCancel
+                onCancel = viewModel::onEnableWidgetDialogCancel,
             )
 
             EnableWidgetDialog(
@@ -460,7 +460,7 @@
                 title = stringResource(id = R.string.work_mode_off_title),
                 positiveButtonText = stringResource(id = R.string.work_mode_turn_on),
                 onConfirm = viewModel::onEnableWorkProfileDialogConfirm,
-                onCancel = viewModel::onEnableWorkProfileDialogCancel
+                onCancel = viewModel::onEnableWorkProfileDialogCancel,
             )
         }
 
@@ -509,7 +509,7 @@
             imageVector = Icons.Outlined.Widgets,
             contentDescription = null,
             tint = colors.primary,
-            modifier = Modifier.size(32.dp)
+            modifier = Modifier.size(32.dp),
         )
         Spacer(modifier = Modifier.height(16.dp))
         Text(
@@ -527,7 +527,7 @@
                 Modifier.padding(horizontal = 26.dp, vertical = 16.dp)
                     .widthIn(min = 200.dp)
                     .heightIn(min = 56.dp),
-            onClick = { onButtonClicked() }
+            onClick = { onButtonClicked() },
         ) {
             Text(
                 stringResource(R.string.communal_widgets_disclaimer_button),
@@ -540,7 +540,7 @@
 @Composable
 private fun ObserveScrollEffect(
     gridState: LazyGridState,
-    communalViewModel: BaseCommunalViewModel
+    communalViewModel: BaseCommunalViewModel,
 ) {
 
     LaunchedEffect(gridState) {
@@ -667,7 +667,7 @@
             rememberGridDragDropState(
                 gridState = gridState,
                 contentListState = contentListState,
-                updateDragPositionForRemove = updateDragPositionForRemove
+                updateDragPositionForRemove = updateDragPositionForRemove,
             )
         gridModifier =
             gridModifier
@@ -677,7 +677,7 @@
                     LocalLayoutDirection.current,
                     screenWidth,
                     contentOffset,
-                    viewModel
+                    viewModel,
                 )
         // for widgets dropped from other activities
         val dragAndDropTargetState =
@@ -709,11 +709,7 @@
             contentType = { _, item -> item.key },
             span = { _, item -> GridItemSpan(item.size.span) },
         ) { index, item ->
-            val size =
-                SizeF(
-                    Dimensions.CardWidth.value,
-                    item.size.dp().value,
-                )
+            val size = SizeF(Dimensions.CardWidth.value, item.size.dp().value)
             val cardModifier = Modifier.requiredSize(width = size.width.dp, height = size.height.dp)
             if (viewModel.isEditMode && dragDropState != null) {
                 val selected = item.key == selectedKey.value
@@ -765,16 +761,13 @@
  * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available.
  */
 @Composable
-private fun EmptyStateCta(
-    contentPadding: PaddingValues,
-    viewModel: BaseCommunalViewModel,
-) {
+private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunalViewModel) {
     val colors = LocalAndroidColorScheme.current
     Card(
         modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
         border = BorderStroke(3.adjustedDp, colors.secondary),
-        shape = RoundedCornerShape(size = 80.adjustedDp)
+        shape = RoundedCornerShape(size = 80.adjustedDp),
     ) {
         Column(
             modifier = Modifier.fillMaxSize().padding(horizontal = 110.adjustedDp),
@@ -788,10 +781,7 @@
                 textAlign = TextAlign.Center,
                 color = colors.secondary,
             )
-            Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.Center,
-            ) {
+            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
                 Button(
                     modifier = Modifier.height(56.dp),
                     colors =
@@ -799,17 +789,13 @@
                             containerColor = colors.primary,
                             contentColor = colors.onPrimary,
                         ),
-                    onClick = {
-                        viewModel.onOpenWidgetEditor(
-                            shouldOpenWidgetPickerOnStart = true,
-                        )
-                    },
+                    onClick = { viewModel.onOpenWidgetEditor(shouldOpenWidgetPickerOnStart = true) },
                 ) {
                     Icon(
                         imageVector = Icons.Default.Add,
                         contentDescription =
                             stringResource(R.string.label_for_button_in_empty_state_cta),
-                        modifier = Modifier.size(24.dp)
+                        modifier = Modifier.size(24.dp),
                     )
                     Spacer(Modifier.width(ButtonDefaults.IconSpacing))
                     Text(
@@ -835,7 +821,7 @@
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
     setRemoveButtonCoordinates: (coordinates: LayoutCoordinates?) -> Unit,
     onOpenWidgetPicker: () -> Unit,
-    onEditDone: () -> Unit
+    onEditDone: () -> Unit,
 ) {
     if (!removeEnabled) {
         // Clear any existing coordinates when remove is not enabled.
@@ -844,7 +830,7 @@
     val removeButtonAlpha: Float by
         animateFloatAsState(
             targetValue = if (removeEnabled) 1f else 0.5f,
-            label = "RemoveButtonAlphaAnimation"
+            label = "RemoveButtonAlphaAnimation",
         )
 
     Box(
@@ -855,7 +841,7 @@
                     start = Dimensions.ToolbarPaddingHorizontal,
                     end = Dimensions.ToolbarPaddingHorizontal,
                 )
-                .onSizeChanged { setToolbarSize(it) },
+                .onSizeChanged { setToolbarSize(it) }
     ) {
         val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
         ToolbarButton(
@@ -864,16 +850,14 @@
             onClick = onOpenWidgetPicker,
         ) {
             Icon(Icons.Default.Add, null)
-            Text(
-                text = addWidgetText,
-            )
+            Text(text = addWidgetText)
         }
 
         AnimatedVisibility(
             modifier = Modifier.align(Alignment.Center),
             visible = removeEnabled,
             enter = fadeIn(),
-            exit = fadeOut()
+            exit = fadeOut(),
         ) {
             Button(
                 onClick = onRemoveClicked,
@@ -887,20 +871,18 @@
                             if (removeEnabled) {
                                 setRemoveButtonCoordinates(it)
                             }
-                        }
+                        },
             ) {
                 Row(
                     horizontalArrangement =
                         Arrangement.spacedBy(
                             ButtonDefaults.IconSpacing,
-                            Alignment.CenterHorizontally
+                            Alignment.CenterHorizontally,
                         ),
-                    verticalAlignment = Alignment.CenterVertically
+                    verticalAlignment = Alignment.CenterVertically,
                 ) {
                     Icon(Icons.Default.Close, contentDescription = null)
-                    Text(
-                        text = stringResource(R.string.button_to_remove_widget),
-                    )
+                    Text(text = stringResource(R.string.button_to_remove_widget))
                 }
             }
         }
@@ -911,9 +893,7 @@
             onClick = onEditDone,
         ) {
             Icon(Icons.Default.Check, contentDescription = null)
-            Text(
-                text = stringResource(R.string.hub_mode_editing_exit_button_text),
-            )
+            Text(text = stringResource(R.string.hub_mode_editing_exit_button_text))
         }
     }
 }
@@ -926,14 +906,14 @@
     isPrimary: Boolean = true,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
-    content: @Composable RowScope.() -> Unit
+    content: @Composable RowScope.() -> Unit,
 ) {
     val colors = LocalAndroidColorScheme.current
     AnimatedVisibility(
         visible = isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         Button(
             onClick = onClick,
@@ -943,7 +923,7 @@
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -954,21 +934,18 @@
         visible = !isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         OutlinedButton(
             onClick = onClick,
-            colors =
-                ButtonDefaults.outlinedButtonColors(
-                    contentColor = colors.onPrimaryContainer,
-                ),
+            colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.onPrimaryContainer),
             border = BorderStroke(width = 2.0.dp, color = colors.primary),
             contentPadding = Dimensions.ButtonPadding,
         ) {
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -1041,7 +1018,7 @@
                     size =
                         Size(width = size.width + padding * 2, height = size.height + padding * 2),
                     cornerRadius = CornerRadius(37.adjustedDp.toPx()),
-                    style = Stroke(width = 3.adjustedDp.toPx())
+                    style = Stroke(width = 3.adjustedDp.toPx()),
                 )
             }
     )
@@ -1061,7 +1038,7 @@
                 containerColor = colors.primary,
                 contentColor = colors.onPrimary,
             ),
-        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp)
+        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp),
     ) {
         Column(
             modifier =
@@ -1081,7 +1058,7 @@
                 style = MaterialTheme.typography.titleLarge,
                 fontSize = nonScalableTextSize(22.dp),
                 lineHeight = nonScalableTextSize(28.dp),
-                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F)
+                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F),
             )
             Spacer(modifier = Modifier.size(16.adjustedDp))
             Row(
@@ -1093,15 +1070,12 @@
                     LocalDensity provides
                         Density(
                             LocalDensity.current.density,
-                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f)
+                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f),
                         )
                 ) {
                     OutlinedButton(
                         modifier = Modifier.fillMaxHeight().weight(1F),
-                        colors =
-                            ButtonDefaults.buttonColors(
-                                contentColor = colors.onPrimary,
-                            ),
+                        colors = ButtonDefaults.buttonColors(contentColor = colors.onPrimary),
                         border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
                         onClick = viewModel::onDismissCtaTile,
                         contentPadding = PaddingValues(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1259,7 +1233,7 @@
                 visible = selected,
                 model = model,
                 widgetConfigurator = widgetConfigurator,
-                modifier = Modifier.align(Alignment.BottomEnd)
+                modifier = Modifier.align(Alignment.BottomEnd),
             )
         }
     }
@@ -1289,14 +1263,14 @@
                     containerColor = colors.primary,
                     contentColor = colors.onPrimary,
                     disabledContainerColor = Color.Transparent,
-                    disabledContentColor = Color.Transparent
+                    disabledContentColor = Color.Transparent,
                 ),
             onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } },
         ) {
             Icon(
                 imageVector = Icons.Outlined.Edit,
                 contentDescription = stringResource(id = R.string.edit_widget),
-                modifier = Modifier.padding(12.adjustedDp)
+                modifier = Modifier.padding(12.adjustedDp),
             )
         }
     }
@@ -1323,13 +1297,13 @@
                 .background(
                     color = MaterialTheme.colorScheme.surfaceVariant,
                     shape =
-                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
                 )
                 .clickable(
                     enabled = !viewModel.isEditMode,
                     interactionSource = null,
                     indication = null,
-                    onClick = viewModel::onOpenEnableWidgetDialog
+                    onClick = viewModel::onOpenEnableWidgetDialog,
                 ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1360,7 +1334,7 @@
         modifier =
             modifier.background(
                 color = MaterialTheme.colorScheme.surfaceVariant,
-                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
             ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1418,7 +1392,7 @@
                             MotionEvent.ACTION_MOVE,
                             change.position.x,
                             change.position.y,
-                            0
+                            0,
                         )
                     viewModel.mediaHost.hostView.dispatchTouchEvent(event)
                     event.recycle()
@@ -1429,12 +1403,12 @@
                 layoutParams =
                     FrameLayout.LayoutParams(
                         FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT
+                        FrameLayout.LayoutParams.MATCH_PARENT,
                     )
             }
             viewModel.mediaHost.hostView
         },
-        onReset = {}
+        onReset = {},
     )
 }
 
@@ -1462,7 +1436,7 @@
                             ) {
                                 viewModel.changeScene(
                                     CommunalScenes.Blank,
-                                    "closed by accessibility"
+                                    "closed by accessibility",
                                 )
                                 true
                             },
@@ -1471,7 +1445,7 @@
                             ) {
                                 viewModel.onOpenWidgetEditor()
                                 true
-                            }
+                            },
                         )
                 }
             }
@@ -1514,7 +1488,7 @@
         start = Dimensions.ToolbarPaddingHorizontal,
         end = Dimensions.ToolbarPaddingHorizontal,
         top = verticalPadding + toolbarHeight,
-        bottom = verticalPadding
+        bottom = verticalPadding,
     )
 }
 
@@ -1523,7 +1497,7 @@
     return with(LocalDensity.current) {
         ContentPaddingInPx(
             start = paddingValues.calculateStartPadding(LocalLayoutDirection.current).toPx(),
-            top = paddingValues.calculateTopPadding().toPx()
+            top = paddingValues.calculateTopPadding().toPx(),
         )
     }
 }
@@ -1536,7 +1510,7 @@
 fun isPointerWithinEnabledRemoveButton(
     removeEnabled: Boolean,
     offset: Offset?,
-    containerToCheck: LayoutCoordinates?
+    containerToCheck: LayoutCoordinates?,
 ): Boolean {
     if (!removeEnabled || offset == null || containerToCheck == null) {
         return false
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
index e41a7df..a88ad94 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt
@@ -21,11 +21,10 @@
 import androidx.compose.ui.Modifier
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
+import com.android.systemui.communal.ui.viewmodel.CommunalUserActionsViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
 import com.android.systemui.dagger.SysUISingleton
@@ -33,38 +32,32 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.Scene
 import javax.inject.Inject
-import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 
 /** The communal scene shows glanceable hub when the device is locked and docked. */
 @SysUISingleton
 class CommunalScene
 @Inject
 constructor(
-    private val viewModel: CommunalViewModel,
+    private val contentViewModel: CommunalViewModel,
+    actionsViewModelFactory: CommunalUserActionsViewModel.Factory,
     private val communalColors: CommunalColors,
     private val communalContent: CommunalContent,
 ) : ExclusiveActivatable(), Scene {
     override val key = Scenes.Communal
 
-    override val userActions: Flow<Map<UserAction, UserActionResult>> =
-        MutableStateFlow(
-                mapOf(
-                    Swipe(SwipeDirection.End) to Scenes.Lockscreen,
-                )
-            )
-            .asStateFlow()
+    private val actionsViewModel: CommunalUserActionsViewModel = actionsViewModelFactory.create()
+
+    override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
 
     override suspend fun onActivated(): Nothing {
-        awaitCancellation()
+        actionsViewModel.activate()
     }
 
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val backgroundType by
-            viewModel.communalBackground.collectAsStateWithLifecycle(
+            contentViewModel.communalBackground.collectAsStateWithLifecycle(
                 initialValue = CommunalBackgroundType.ANIMATED
             )
 
@@ -72,7 +65,7 @@
             backgroundType = backgroundType,
             colors = communalColors,
             content = communalContent,
-            viewModel = viewModel,
+            viewModel = contentViewModel,
             modifier = modifier.horizontalNestedScrollToScene(),
         )
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 4162891..6f1349f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.shade.ui.composable
 
-import android.view.HapticFeedbackConstants
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -40,20 +39,17 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexContentPicker
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.systemui.scene.shared.model.Scenes
 
 /** Renders a lightweight shade UI container, as an overlay. */
 @Composable
@@ -62,13 +58,6 @@
     modifier: Modifier = Modifier,
     content: @Composable () -> Unit,
 ) {
-    val view = LocalView.current
-    LaunchedEffect(Unit) {
-        if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
-            view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
-        }
-    }
-
     Box(modifier) {
         Scrim(onClicked = onScrimClicked)
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index db0fe3e..ef415b1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade.ui.composable
 
-import android.view.HapticFeedbackConstants
 import android.view.ViewGroup
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
@@ -60,7 +59,6 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.res.colorResource
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.dp
@@ -226,12 +224,6 @@
     shadeSession: SaveableSession,
     usingCollapsedLandscapeMedia: Boolean,
 ) {
-    val view = LocalView.current
-    LaunchedEffect(Unit) {
-        if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
-            view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
-        }
-    }
 
     val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle()
     when (shadeMode) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 9891025..367faed 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -188,38 +188,47 @@
         return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
     }
 
+    private fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
+        if (startedPosition == null) return null
+        return layoutImpl.swipeSourceDetector.source(
+            layoutSize = layoutImpl.lastSize,
+            position = startedPosition.round(),
+            density = layoutImpl.density,
+            orientation = orientation,
+        )
+    }
+
+    private fun resolveSwipe(
+        pointersDown: Int,
+        fromSource: SwipeSource.Resolved?,
+        isUpOrLeft: Boolean,
+    ): Swipe.Resolved {
+        return Swipe.Resolved(
+            direction =
+                when (orientation) {
+                    Orientation.Horizontal ->
+                        if (isUpOrLeft) {
+                            SwipeDirection.Resolved.Left
+                        } else {
+                            SwipeDirection.Resolved.Right
+                        }
+
+                    Orientation.Vertical ->
+                        if (isUpOrLeft) {
+                            SwipeDirection.Resolved.Up
+                        } else {
+                            SwipeDirection.Resolved.Down
+                        }
+                },
+            pointerCount = pointersDown,
+            fromSource = fromSource,
+        )
+    }
+
     private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes {
-        val fromSource =
-            startedPosition?.let { position ->
-                layoutImpl.swipeSourceDetector.source(
-                    layoutImpl.lastSize,
-                    position.round(),
-                    layoutImpl.density,
-                    orientation,
-                )
-            }
-
-        val upOrLeft =
-            Swipe.Resolved(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Resolved.Left
-                        Orientation.Vertical -> SwipeDirection.Resolved.Up
-                    },
-                pointerCount = pointersDown,
-                fromSource = fromSource,
-            )
-
-        val downOrRight =
-            Swipe.Resolved(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Resolved.Right
-                        Orientation.Vertical -> SwipeDirection.Resolved.Down
-                    },
-                pointerCount = pointersDown,
-                fromSource = fromSource,
-            )
+        val fromSource = resolveSwipeSource(startedPosition)
+        val upOrLeft = resolveSwipe(pointersDown, fromSource, isUpOrLeft = true)
+        val downOrRight = resolveSwipe(pointersDown, fromSource, isUpOrLeft = false)
 
         return if (fromSource == null) {
             Swipes(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index c163c6f..0490a26 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -350,7 +350,7 @@
         testScope.runTest {
             underTest.performDotFeedback(null)
 
-            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR)
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE)
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index e25c1a7..d5020a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -109,7 +109,9 @@
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
         underTest.start()
         kosmos.communalSceneRepository.setTransitionState(sceneTransitions)
-        testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) }
+        testScope.launch {
+            keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN, testSetup = true)
+        }
     }
 
     /** Transition from blank to glanceable hub. This is the default case. */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
new file mode 100644
index 0000000..58b59ff
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.communal.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class CommunalUserActionsViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: CommunalUserActionsViewModel
+
+    @Before
+    fun setUp() {
+        underTest = kosmos.communalUserActionsViewModel
+        underTest.activateIn(testScope)
+    }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun actions_singleShade() =
+        testScope.runTest {
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = true,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun actions_splitShade() =
+        testScope.runTest {
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = true,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun actions_dualShade() =
+        testScope.runTest {
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Dual,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Overlays.NotificationsShade))
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Dual,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Overlays.NotificationsShade))
+        }
+
+    private fun TestScope.setUpState(
+        isShadeTouchable: Boolean,
+        isDeviceUnlocked: Boolean,
+        shadeMode: ShadeMode,
+    ) {
+        if (isShadeTouchable) {
+            kosmos.powerInteractor.setAwakeForTest()
+        } else {
+            kosmos.powerInteractor.setAsleepForTest()
+        }
+
+        if (isDeviceUnlocked) {
+            unlockDevice()
+        } else {
+            lockDevice()
+        }
+
+        if (shadeMode == ShadeMode.Dual) {
+            assertThat(DualShade.isEnabled).isTrue()
+        } else {
+            assertThat(DualShade.isEnabled).isFalse()
+            kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split)
+        }
+        runCurrent()
+    }
+
+    private fun TestScope.lockDevice() {
+        val deviceUnlockStatus by
+            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+
+        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+        assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+        runCurrent()
+    }
+
+    private fun TestScope.unlockDevice() {
+        val deviceUnlockStatus by
+            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+        kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
+        runCurrent()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index ab33269..d7fe263 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.education.domain.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationManager
 import android.content.applicationContext
-import android.widget.Toast
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -34,11 +34,13 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,6 +53,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -63,10 +66,12 @@
     private val minDurationForNextEdu =
         KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
     private lateinit var underTest: ContextualEduUiCoordinator
-    @Mock private lateinit var toast: Toast
+    @Mock private lateinit var dialog: Dialog
     @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
     @get:Rule val mockitoRule = MockitoJUnit.rule()
     private var toastContent = ""
+    private val timeoutMillis = 3500L
 
     @Before
     fun setUp() {
@@ -75,30 +80,35 @@
             interactor.updateTouchpadFirstConnectionTime()
         }
 
+        whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any()))
+            .thenReturn(timeoutMillis.toInt())
+
         val viewModel =
             ContextualEduViewModel(
                 kosmos.applicationContext.resources,
-                kosmos.keyboardTouchpadEduInteractor
+                kosmos.keyboardTouchpadEduInteractor,
+                accessibilityManagerWrapper,
             )
+
         underTest =
             ContextualEduUiCoordinator(
                 kosmos.applicationCoroutineScope,
                 viewModel,
                 kosmos.applicationContext,
                 notificationManager
-            ) { content ->
-                toastContent = content
-                toast
+            ) { model ->
+                toastContent = model.message
+                dialog
             }
         underTest.start()
         kosmos.keyboardTouchpadEduInteractor.start()
     }
 
     @Test
-    fun showToastOnNewEdu() =
+    fun showDialogOnNewEdu() =
         testScope.runTest {
             triggerEducation(BACK)
-            verify(toast).show()
+            verify(dialog).show()
         }
 
     @Test
@@ -111,6 +121,14 @@
         }
 
     @Test
+    fun dismissDialogAfterTimeout() =
+        testScope.runTest {
+            triggerEducation(BACK)
+            advanceTimeBy(timeoutMillis + 1)
+            verify(dialog).dismiss()
+        }
+
+    @Test
     fun verifyBackEduToastContent() =
         testScope.runTest {
             triggerEducation(BACK)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index 29035ce..d97909a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -19,17 +19,22 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.DismissAction
 import com.android.systemui.keyguard.shared.model.KeyguardDone
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -38,11 +43,13 @@
 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
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -80,6 +87,7 @@
                 alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
                 shadeInteractor = { kosmos.shadeInteractor },
                 keyguardInteractor = { kosmos.keyguardInteractor },
+                sceneInteractor = { kosmos.sceneInteractor },
             )
     }
 
@@ -178,7 +186,11 @@
             )
             assertThat(executeDismissAction).isNull()
 
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
             kosmos.setSceneTransition(Idle(Scenes.Gone))
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
 
             assertThat(executeDismissAction).isNotNull()
         }
@@ -301,4 +313,78 @@
             underTest.setKeyguardDone(KeyguardDone.IMMEDIATE)
             assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE)
         }
+
+    @Test
+    @EnableSceneContainer
+    fun dismissAction_executesBeforeItsReset_sceneContainerOn_swipeAuth_fromQsScene() =
+        testScope.runTest {
+            val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(currentScene!!)
+                )
+            kosmos.sceneInteractor.setTransitionState(transitionState)
+            val executeDismissAction by collectLastValue(underTest.executeDismissAction)
+            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.None
+            )
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+            assertThat(canSwipeToEnter).isTrue()
+            kosmos.sceneInteractor.changeScene(Scenes.QuickSettings, "")
+            transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings)
+            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            val dismissAction =
+                DismissAction.RunImmediately(
+                    onDismissAction = { KeyguardDone.LATER },
+                    onCancelAction = {},
+                    message = "message",
+                    willAnimateOnLockscreen = true,
+                )
+            underTest.setDismissAction(dismissAction)
+            // Should still be null because the transition to Gone has not yet happened.
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            transitionState.value =
+                ObservableTransitionState.Transition.ChangeScene(
+                    fromScene = Scenes.QuickSettings,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.QuickSettings),
+                    currentOverlays = emptySet(),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            runCurrent()
+            assertThat(executeDismissAction).isNull()
+            assertThat(resetDismissAction).isNull()
+
+            transitionState.value =
+                ObservableTransitionState.Transition.ChangeScene(
+                    fromScene = Scenes.QuickSettings,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.Gone),
+                    currentOverlays = emptySet(),
+                    progress = flowOf(1f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            runCurrent()
+            assertThat(executeDismissAction).isNotNull()
+            assertThat(resetDismissAction).isNull()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
index 129752e..aab46d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.testKosmos
 import org.junit.Before
 import org.junit.Test
@@ -44,6 +45,7 @@
             KeyguardBlueprintViewModel(
                 handler = kosmos.fakeExecutorHandler,
                 keyguardBlueprintInteractor = keyguardBlueprintInteractor,
+                keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index e6ea64f..d0da2e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -89,9 +89,12 @@
     }
 
     @Test
-    fun lockscreenFadeOut() =
+    fun lockscreenFadeOut_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
             repository.sendTransitionSteps(
                 steps =
                     listOf(
@@ -104,10 +107,34 @@
                     ),
                 testScope = testScope,
             )
-            // Only 5 values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
             assertThat(values.size).isEqualTo(5)
-            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+            assertThat(values[0]).isEqualTo(1f)
+            assertThat(values[1]).isEqualTo(1f)
+            assertThat(values[2]).isIn(Range.open(0f, 1f))
+            assertThat(values[3]).isIn(Range.open(0f, 1f))
+            assertThat(values[4]).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenFadeOut_shadeExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.1f),
+                        step(.4f),
+                        step(.7f), // ...up to here
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
+            values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
     @Test
@@ -115,7 +142,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -138,7 +165,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -171,7 +198,7 @@
                     listOf(
                         step(0f, TransitionState.STARTED),
                         step(.5f),
-                        step(1f, TransitionState.FINISHED)
+                        step(1f, TransitionState.FINISHED),
                     ),
                 testScope = testScope,
             )
@@ -228,7 +255,7 @@
             to = KeyguardState.OCCLUDED,
             value = value,
             transitionState = state,
-            ownerName = "LockscreenToOccludedTransitionViewModelTest"
+            ownerName = "LockscreenToOccludedTransitionViewModelTest",
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index a330cf0..fb1bf28 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -147,7 +147,7 @@
             }
         }
 
-        private fun expectedLeftDestination(
+        private fun expectedStartDestination(
             isCommunalAvailable: Boolean,
             isShadeTouchable: Boolean,
         ): SceneKey? {
@@ -246,17 +246,17 @@
                     )
                 )
 
-            val leftScene by
+            val startScene by
                 collectLastValue(
-                    (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
-                        scene ->
-                        kosmos.sceneInteractor.resolveSceneFamily(scene)
-                    } ?: flowOf(null)
+                    (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
+                        ?.toScene
+                        ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
+                        ?: flowOf(null)
                 )
 
-            assertThat(leftScene)
+            assertThat(startScene)
                 .isEqualTo(
-                    expectedLeftDestination(
+                    expectedStartDestination(
                         isCommunalAvailable = isCommunalAvailable,
                         isShadeTouchable = isShadeTouchable,
                     )
@@ -341,17 +341,17 @@
                     )
                 )
 
-            val leftScene by
+            val startScene by
                 collectLastValue(
-                    (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
-                        scene ->
-                        kosmos.sceneInteractor.resolveSceneFamily(scene)
-                    } ?: flowOf(null)
+                    (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
+                        ?.toScene
+                        ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
+                        ?: flowOf(null)
                 )
 
-            assertThat(leftScene)
+            assertThat(startScene)
                 .isEqualTo(
-                    expectedLeftDestination(
+                    expectedStartDestination(
                         isCommunalAvailable = isCommunalAvailable,
                         isShadeTouchable = isShadeTouchable,
                     )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..0b7a38e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.platform.test.flag.junit.FlagsParameterization
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class OffToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    lateinit var underTest: OffToLockscreenTransitionViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.offToLockscreenTransitionViewModel
+    }
+
+    @Test
+    fun lockscreenAlpha() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.lockscreenAlpha)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            assertThat(alpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.66f))
+            assertThat(alpha).isIn(Range.open(.1f, .9f))
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(alpha).isEqualTo(1f)
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.OFF,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "OffToLockscreenTransitionViewModelTest",
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
index 3e3aa4f..e12c67b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
@@ -18,7 +18,7 @@
 import com.android.wm.shell.recents.RecentTasks
 import com.android.wm.shell.shared.GroupedRecentTaskInfo
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import java.util.function.Consumer
@@ -268,7 +268,7 @@
         GroupedRecentTaskInfo.forSplitTasks(
             createTaskInfo(taskId1, userId1, isVisible),
             createTaskInfo(taskId2, userId2, isVisible),
-            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_50_50)
+            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_2_50_50)
         )
 
     private fun createTaskInfo(taskId: Int, userId: Int, isVisible: Boolean = false) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt
new file mode 100644
index 0000000..4bbdfa4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.viewmodel
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.testing.TestLifecycleOwner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestResult
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+
+@OptIn(ExperimentalCoroutinesApi::class)
+abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() {
+    protected val kosmos = testKosmos()
+
+    protected val lifecycleOwner =
+        TestLifecycleOwner(
+            initialState = Lifecycle.State.CREATED,
+            coroutineDispatcher = kosmos.testDispatcher,
+        )
+
+    protected val underTest by lazy {
+        kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope)
+    }
+
+    @Before
+    fun setUp() {
+        Dispatchers.setMain(kosmos.testDispatcher)
+    }
+
+    @After
+    fun teardown() {
+        Dispatchers.resetMain()
+    }
+
+    protected inline fun TestScope.testWithinLifecycle(
+        crossinline block: suspend TestScope.() -> TestResult
+    ): TestResult {
+        return runTest {
+            lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
+            lifecycleOwner.lifecycleScope.launch { underTest.activate() }
+            block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
new file mode 100644
index 0000000..acd69af
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.viewmodel
+
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.sysuiStatusBarStateController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+@RunWithLooper
+class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) :
+    AbstractQSFragmentComposeViewModelTest() {
+
+    @Test
+    fun forceQs_orRealExpansion() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                val expansionState by collectLastValue(underTest.expansionState)
+
+                with(testData) {
+                    sysuiStatusBarStateController.setState(statusBarState)
+                    underTest.isQSExpanded = expanded
+                    underTest.isStackScrollerOverscrolling = stackScrollerOverScrolling
+                    fakeDeviceEntryRepository.setBypassEnabled(bypassEnabled)
+                    underTest.isTransitioningToFullShade = transitioningToFullShade
+                    underTest.isInSplitShade = inSplitShade
+
+                    underTest.qsExpansionValue = EXPANSION
+                    assertThat(expansionState!!.progress)
+                        .isEqualTo(if (expectedForceQS) 1f else EXPANSION)
+                }
+            }
+        }
+
+    data class TestData(
+        val statusBarState: Int,
+        val expanded: Boolean,
+        val stackScrollerOverScrolling: Boolean,
+        val bypassEnabled: Boolean,
+        val transitioningToFullShade: Boolean,
+        val inSplitShade: Boolean,
+    ) {
+        private val inKeyguard = statusBarState == StatusBarState.KEYGUARD
+
+        private val showCollapsedOnKeyguard =
+            bypassEnabled || (transitioningToFullShade && !inSplitShade)
+
+        val expectedForceQS =
+            (expanded || stackScrollerOverScrolling) && (inKeyguard && !showCollapsedOnKeyguard)
+    }
+
+    companion object {
+        private const val EXPANSION = 0.3f
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun createTestData(): List<TestData> {
+            return statusBarStates.flatMap { statusBarState ->
+                (0u..31u).map { bitfield ->
+                    TestData(
+                        statusBarState,
+                        expanded = (bitfield and 1u) == 1u,
+                        stackScrollerOverScrolling = (bitfield and 2u) == 2u,
+                        bypassEnabled = (bitfield and 4u) == 4u,
+                        transitioningToFullShade = (bitfield and 8u) == 8u,
+                        inSplitShade = (bitfield and 16u) == 16u,
+                    )
+                }
+            }
+        }
+
+        private val statusBarStates =
+            setOf(StatusBarState.SHADE, StatusBarState.KEYGUARD, StatusBarState.SHADE_LOCKED)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 6f20e70..c19e4b8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -19,64 +19,28 @@
 import android.app.StatusBarManager
 import android.content.testableContext
 import android.testing.TestableLooper.RunWithLooper
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.fgsManagerController
+import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
 import com.android.systemui.res.R
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
 import com.android.systemui.statusbar.sysuiStatusBarStateController
-import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestResult
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
-class QSFragmentComposeViewModelTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-
-    private val lifecycleOwner =
-        TestLifecycleOwner(
-            initialState = Lifecycle.State.CREATED,
-            coroutineDispatcher = kosmos.testDispatcher,
-        )
-
-    private val underTest by lazy {
-        kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope)
-    }
-
-    @Before
-    fun setUp() {
-        Dispatchers.setMain(kosmos.testDispatcher)
-    }
-
-    @After
-    fun teardown() {
-        Dispatchers.resetMain()
-    }
+class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() {
 
     @Test
     fun qsExpansionValueChanges_correctExpansionState() =
@@ -205,16 +169,30 @@
             }
         }
 
-    private inline fun TestScope.testWithinLifecycle(
-        crossinline block: suspend TestScope.() -> TestResult
-    ): TestResult {
-        return runTest {
-            lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
-            block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
+    @Test
+    fun squishinessInExpansion_setInInteractor() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                val squishiness by collectLastValue(tileSquishinessInteractor.squishiness)
+
+                underTest.squishinessFractionValue = 0.3f
+                assertThat(squishiness).isWithin(epsilon).of(0.3f.constrainSquishiness())
+
+                underTest.squishinessFractionValue = 0f
+                assertThat(squishiness).isWithin(epsilon).of(0f.constrainSquishiness())
+
+                underTest.squishinessFractionValue = 1f
+                assertThat(squishiness).isWithin(epsilon).of(1f.constrainSquishiness())
+            }
         }
-    }
 
     companion object {
         private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS
+
+        private fun Float.constrainSquishiness(): Float {
+            return (0.1f + this * 0.9f).coerceIn(0f, 1f)
+        }
+
+        private const val epsilon = 0.001f
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
index 9e90090..a9a527f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -22,10 +22,8 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
 import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
-import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -44,8 +42,7 @@
                 }
         }
 
-    private val underTest =
-        with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
+    private val underTest = kosmos.infiniteGridLayout
 
     @Test
     fun correctPagination_underOnePage_sameOrder() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index c9e958d..d2bf9b88 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -50,9 +50,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
@@ -185,7 +183,6 @@
     fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
         testScope.runTest {
             val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions)
-            val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
             kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
             kosmos.assertCurrentScene(Scenes.Lockscreen)
 
@@ -195,9 +192,8 @@
 
             val upDestinationSceneKey =
                 (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
-            assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
-            assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
-            kosmos.emulateUserDrivenTransition(to = homeScene)
+            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen)
+            kosmos.emulateUserDrivenTransition(to = Scenes.Lockscreen)
         }
 
     @Test
@@ -205,7 +201,6 @@
         testScope.runTest {
             val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions)
             val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter)
-            val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
 
             kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
 
@@ -222,9 +217,8 @@
 
             val upDestinationSceneKey =
                 (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene
-            assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
-            assertThat(homeScene).isEqualTo(Scenes.Gone)
-            kosmos.emulateUserDrivenTransition(to = homeScene)
+            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
+            kosmos.emulateUserDrivenTransition(to = Scenes.Gone)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
new file mode 100644
index 0000000..664315d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.HapticFeedbackConstants
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.sceneContainerHapticsViewModelFactory
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class SceneContainerHapticsViewModelTest() : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val msdlPlayer = kosmos.fakeMSDLPlayer
+    private val view = mock<View>()
+
+    private lateinit var underTest: SceneContainerHapticsViewModel
+
+    @Before
+    fun setup() {
+        underTest = kosmos.sceneContainerHapticsViewModelFactory.create(view)
+        underTest.activateIn(testScope)
+    }
+
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    @DisableFlags(Flags.FLAG_DUAL_SHADE)
+    @Test
+    fun onValidSceneTransition_withMSDL_playsMSDLShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN a valid scene transition to play haptics
+            val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+            runCurrent()
+
+            // THEN the expected token plays without interaction properties
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    @DisableFlags(Flags.FLAG_DUAL_SHADE)
+    @Test
+    fun onInValidSceneTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN an invalid scene transition to play haptics
+            val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+            runCurrent()
+
+            // THEN the no token plays with no interaction properties
+            assertThat(msdlPlayer.latestTokenPlayed).isNull()
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+    @Test
+    fun onValidSceneTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN a valid scene transition to play haptics
+            val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+            runCurrent()
+
+            // THEN the expected haptic feedback constant plays
+            verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+        }
+
+    @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+    @Test
+    fun onInValidSceneTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN an invalid scene transition to play haptics
+            val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+            runCurrent()
+
+            // THEN the view does not play a haptic feedback constant
+            verifyZeroInteractions(view)
+        }
+
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+    @Test
+    fun onValidOverlayTransition_withMSDL_playsMSDLShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN a valid scene transition to play haptics
+            val validTransition =
+                createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+            runCurrent()
+
+            // THEN the expected token plays without interaction properties
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+    @Test
+    fun onInValidOverlayTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN an invalid scene transition to play haptics
+            val invalidTransition =
+                createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+            runCurrent()
+
+            // THEN the no token plays with no interaction properties
+            assertThat(msdlPlayer.latestTokenPlayed).isNull()
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @EnableFlags(Flags.FLAG_DUAL_SHADE)
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    @Test
+    fun onValidOverlayTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN a valid scene transition to play haptics
+            val validTransition =
+                createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+            runCurrent()
+
+            // THEN the expected haptic feedback constant plays
+            verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+        }
+
+    @EnableFlags(Flags.FLAG_DUAL_SHADE)
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    @Test
+    fun onInValidOverlayTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+        testScope.runTest {
+            // GIVEN an invalid scene transition to play haptics
+            val invalidTransition =
+                createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+            // WHEN the transition occurs
+            sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+            runCurrent()
+
+            // THEN the view does not play a haptic feedback constant
+            verifyZeroInteractions(view)
+        }
+
+    private fun createTransitionState(from: SceneKey, to: ContentKey) =
+        when (to) {
+            is SceneKey ->
+                ObservableTransitionState.Transition(
+                    fromScene = from,
+                    toScene = to,
+                    currentScene = flowOf(from),
+                    progress = MutableStateFlow(0.2f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                )
+            is OverlayKey ->
+                ShowOrHideOverlay(
+                    overlay = to,
+                    fromContent = from,
+                    toContent = to,
+                    currentScene = from,
+                    currentOverlays = sceneInteractor.currentOverlays,
+                    progress = MutableStateFlow(0.2f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index e60e742..a37f511 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -21,23 +21,21 @@
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.view.MotionEvent
+import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.classifier.fakeFalsingManager
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.power.data.repository.fakePowerRepository
-import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.fakeOverlaysByKeys
 import com.android.systemui.scene.sceneContainerConfig
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -72,6 +70,7 @@
     private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
     private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
     private val falsingManager by lazy { kosmos.fakeFalsingManager }
+    private val view = mock<View>()
 
     private lateinit var underTest: SceneContainerViewModel
 
@@ -81,16 +80,10 @@
     @Before
     fun setUp() {
         underTest =
-            SceneContainerViewModel(
-                sceneInteractor = sceneInteractor,
-                falsingInteractor = kosmos.falsingInteractor,
-                powerInteractor = kosmos.powerInteractor,
-                shadeInteractor = kosmos.shadeInteractor,
-                splitEdgeDetector = kosmos.splitEdgeDetector,
-                logger = kosmos.sceneLogger,
-                gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
-                displayId = kosmos.displayTracker.defaultDisplayId,
-                motionEventHandlerReceiver = { motionEventHandler ->
+            kosmos.sceneContainerViewModelFactory.create(
+                view,
+                kosmos.displayTracker.defaultDisplayId,
+                { motionEventHandler ->
                     this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
                 },
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
index 9f3e126e..15d6881 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
@@ -76,6 +77,7 @@
 
     @Before
     fun setUp() {
+        kosmos.sceneContainerStartable.start()
         underTest.activateIn(testScope)
     }
 
@@ -232,6 +234,20 @@
                 .isEmpty()
         }
 
+    @Test
+    fun upTransitionSceneKey_backToCommunal() =
+        testScope.runTest {
+            val actions by collectLastValue(underTest.actions)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            kosmos.sceneInteractor.changeScene(Scenes.Communal, "")
+            assertThat(currentScene).isEqualTo(Scenes.Communal)
+            kosmos.sceneInteractor.changeScene(Scenes.Shade, "")
+            assertThat(currentScene).isEqualTo(Scenes.Shade)
+
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Communal))
+        }
+
     private fun TestScope.setDeviceEntered(isEntered: Boolean) {
         if (isEntered) {
             // Unlock the device marking the device has entered.
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e94248d..629c94f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -2047,4 +2047,6 @@
     <!-- SliceView icon size -->
     <dimen name="abc_slice_big_pic_min_height">64dp</dimen>
     <dimen name="abc_slice_big_pic_max_height">64dp</dimen>
+
+    <dimen name="contextual_edu_dialog_bottom_margin">70dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 75389b1..c76b35f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3818,6 +3818,8 @@
     <!-- Main text of the one line view of a redacted notification -->
     <string name="redacted_notification_single_line_text">Unlock to view</string>
 
+    <!-- Content description for contextual education dialog [CHAR LIMIT=NONE] -->
+    <string name="contextual_education_dialog_title">Contextual education</string>
     <!-- Education notification title for Back [CHAR_LIMIT=100] -->
     <string name="back_edu_notification_title">Use your touchpad to go back</string>
     <!-- Education notification text for Back [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a02c354..b34d6e4 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1720,4 +1720,10 @@
     <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
         <item name="android:windowLightNavigationBar">true</item>
     </style>
+
+    <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+        <!-- To make the dialog wrap to content when the education text is short -->
+        <item name="windowMinWidthMajor">0%</item>
+        <item name="windowMinWidthMinor">0%</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 55ccaa6..92bc95a 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -70,4 +70,12 @@
      * {@link #onBootCompleted()} will never be called before {@link #start()}. */
     default void onBootCompleted() {
     }
+
+    /** No op implementation that can be used when feature flagging on the Dagger Module level. */
+    CoreStartable NOP = new Nop();
+
+    class Nop implements CoreStartable {
+        @Override
+        public void start() {}
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 76df9c9..fb00d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -75,6 +75,9 @@
  * touches are consumed.
  */
 public class TouchMonitor {
+    // An incrementing id used to identify the touch monitor instance.
+    private static int sNextInstanceId = 0;
+
     private final Logger mLogger;
     // This executor is used to protect {@code mActiveTouchSessions} from being modified
     // concurrently. Any operation that adds or removes values should use this executor.
@@ -138,7 +141,7 @@
                     completer.set(predecessor);
                 }
 
-                if (mActiveTouchSessions.isEmpty()) {
+                if (mActiveTouchSessions.isEmpty() && mInitialized) {
                     if (mStopMonitoringPending) {
                         stopMonitoring(false);
                     } else {
@@ -271,7 +274,7 @@
 
         @Override
         public void onDestroy(LifecycleOwner owner) {
-            stopMonitoring(true);
+            destroy();
         }
     };
 
@@ -279,6 +282,11 @@
      * When invoked, instantiates a new {@link InputSession} to monitor touch events.
      */
     private void startMonitoring() {
+        if (!mInitialized) {
+            mLogger.w("attempting to startMonitoring when not initialized");
+            return;
+        }
+
         mLogger.i("startMonitoring(): monitoring started");
         stopMonitoring(true);
 
@@ -587,7 +595,7 @@
         mDisplayHelper = displayHelper;
         mWindowManagerService = windowManagerService;
         mConfigurationInteractor = configurationInteractor;
-        mLoggingName = loggingName + ":TouchMonitor";
+        mLoggingName = loggingName + ":TouchMonitor[" + sNextInstanceId++ + "]";
         mLogger = new Logger(logBuffer, mLoggingName);
     }
 
@@ -613,7 +621,8 @@
      */
     public void destroy() {
         if (!mInitialized) {
-            throw new IllegalStateException("TouchMonitor not initialized");
+            // In the case that we've already been destroyed, this is a no-op
+            return;
         }
 
         stopMonitoring(true);
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index b8c30fe..d6b9211 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -69,7 +69,7 @@
                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
             )
         } else {
-            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR)
+            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR_DISCRETE)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
new file mode 100644
index 0000000..e35fdfe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.viewmodel
+
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Provides scene container user actions and results. */
+class CommunalUserActionsViewModel
+@AssistedInject
+constructor(
+    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
+    private val shadeInteractor: ShadeInteractor,
+) : UserActionsViewModel() {
+
+    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+        shadeInteractor.isShadeTouchable
+            .flatMapLatestConflated { isShadeTouchable ->
+                if (!isShadeTouchable) {
+                    flowOf(emptyMap())
+                } else {
+                    combine(
+                        deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
+                        shadeInteractor.shadeMode,
+                    ) { isDeviceUnlocked, shadeMode ->
+                        buildList {
+                                val bouncerOrGone =
+                                    if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
+                                add(Swipe.Up to bouncerOrGone)
+
+                                // "Home" is either Lockscreen, or Gone - if the device is entered.
+                                add(Swipe.End to SceneFamilies.Home)
+
+                                addAll(
+                                    when (shadeMode) {
+                                        ShadeMode.Single -> singleShadeActions()
+                                        ShadeMode.Split -> splitShadeActions()
+                                        ShadeMode.Dual -> dualShadeActions()
+                                    }
+                                )
+                            }
+                            .associate { it }
+                    }
+                }
+            }
+            .collect { setActions(it) }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): CommunalUserActionsViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index cbea876..8da4d46 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.dreams.DreamMonitor
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
-import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialCoreStartable
+import com.android.systemui.haptics.msdl.MSDLCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -323,4 +323,9 @@
     @IntoMap
     @ClassKey(BatteryControllerStartable::class)
     abstract fun bindsBatteryControllerStartable(impl: BatteryControllerStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(MSDLCoreStartable::class)
+    abstract fun bindMSDLCoreStartable(impl: MSDLCoreStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
new file mode 100644
index 0000000..287e85c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.education.ui.view
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import android.widget.ToastPresenter
+import com.android.systemui.education.ui.viewmodel.ContextualEduToastViewModel
+import com.android.systemui.res.R
+
+class ContextualEduDialog(context: Context, private val model: ContextualEduToastViewModel) :
+    AlertDialog(context, R.style.ContextualEduDialog) {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setUpWindowProperties()
+        setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.contextual_education_dialog_title))
+        // TODO: b/369791926 - replace the below toast view with a custom dialog view
+        val toastView = ToastPresenter.getTextToastView(context, model.message)
+        setView(toastView)
+        super.onCreate(savedInstanceState)
+    }
+
+    private fun setUpWindowProperties() {
+        window?.apply {
+            setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)
+            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+        }
+        setCanceledOnTouchOutside(false)
+    }
+
+    private fun setWindowPosition() {
+        window?.apply {
+            setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+            this.attributes =
+                WindowManager.LayoutParams().apply {
+                    width = WindowManager.LayoutParams.WRAP_CONTENT
+                    height = WindowManager.LayoutParams.WRAP_CONTENT
+                    copyFrom(attributes)
+                    y =
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.contextual_edu_dialog_bottom_margin
+                        )
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
index e62b26b..913ecdd 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.education.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager
@@ -24,7 +25,6 @@
 import android.content.Intent
 import android.os.Bundle
 import android.os.UserHandle
-import android.widget.Toast
 import androidx.core.app.NotificationCompat
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
@@ -49,7 +49,7 @@
     private val viewModel: ContextualEduViewModel,
     private val context: Context,
     private val notificationManager: NotificationManager,
-    private val createToast: (String) -> Toast
+    private val createDialog: (ContextualEduToastViewModel) -> Dialog,
 ) : CoreStartable {
 
     companion object {
@@ -69,16 +69,23 @@
         viewModel,
         context,
         notificationManager,
-        createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) }
+        createDialog = { model -> ContextualEduDialog(context, model) },
     )
 
+    var dialog: Dialog? = null
+
     override fun start() {
         createEduNotificationChannel()
         applicationScope.launch {
             viewModel.eduContent.collect { contentModel ->
-                when (contentModel) {
-                    is ContextualEduToastViewModel -> showToast(contentModel)
-                    is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                if (contentModel != null) {
+                    when (contentModel) {
+                        is ContextualEduToastViewModel -> showDialog(contentModel)
+                        is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                    }
+                } else {
+                    dialog?.dismiss()
+                    dialog = null
                 }
             }
         }
@@ -95,9 +102,9 @@
         notificationManager.createNotificationChannel(channel)
     }
 
-    private fun showToast(model: ContextualEduToastViewModel) {
-        val toast = createToast(model.message)
-        toast.show()
+    private fun showDialog(model: ContextualEduToastViewModel) {
+        dialog = createDialog(model)
+        dialog?.show()
     }
 
     private fun showNotification(model: ContextualEduNotificationViewModel) {
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
index cd4a8ad..32e7f41 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.education.ui.viewmodel
 
 import android.content.res.Resources
+import android.view.accessibility.AccessibilityManager
 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.contextualeducation.GestureType.HOME
@@ -27,23 +28,63 @@
 import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import javax.inject.Inject
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class ContextualEduViewModel
 @Inject
-constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) {
-    val eduContent: Flow<ContextualEduContentViewModel> =
-        interactor.educationTriggered.filterNotNull().map {
-            if (it.educationUiType == EducationUiType.Notification) {
-                ContextualEduNotificationViewModel(getEduTitle(it), getEduContent(it), it.userId)
-            } else {
-                ContextualEduToastViewModel(getEduContent(it), it.userId)
+constructor(
+    @Main private val resources: Resources,
+    interactor: KeyboardTouchpadEduInteractor,
+    private val accessibilityManagerWrapper: AccessibilityManagerWrapper,
+) {
+
+    companion object {
+        const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500
+    }
+
+    private val timeoutMillis: Long
+        get() =
+            accessibilityManagerWrapper
+                .getRecommendedTimeoutMillis(
+                    DEFAULT_DIALOG_TIMEOUT_MILLIS,
+                    AccessibilityManager.FLAG_CONTENT_TEXT,
+                )
+                .toLong()
+
+    val eduContent: Flow<ContextualEduContentViewModel?> =
+        interactor.educationTriggered
+            .filterNotNull()
+            .map {
+                if (it.educationUiType == EducationUiType.Notification) {
+                    ContextualEduNotificationViewModel(
+                        getEduTitle(it),
+                        getEduContent(it),
+                        it.userId,
+                    )
+                } else {
+                    ContextualEduToastViewModel(getEduContent(it), it.userId)
+                }
+            }
+            .timeout(timeoutMillis, emitAfterTimeout = null)
+
+    private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> {
+        return flatMapLatest {
+            flow {
+                emit(it)
+                delay(timeoutMillis)
+                emit(emitAfterTimeout)
             }
         }
+    }
 
     private fun getEduContent(educationInfo: EducationInfo): String {
         val resourceId =
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
new file mode 100644
index 0000000..58736c60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.msdl
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.google.android.msdl.domain.MSDLPlayer
+import com.google.android.msdl.logging.MSDLHistoryLogger
+import java.io.PrintWriter
+import javax.inject.Inject
+
+@SysUISingleton
+class MSDLCoreStartable @Inject constructor(private val msdlPlayer: MSDLPlayer) : CoreStartable {
+    override fun start() {}
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("MSDLPlayer history of the last ${MSDLHistoryLogger.HISTORY_SIZE} events:")
+        msdlPlayer.getHistory().forEach { event -> pw.println("$event") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 0a38ce0..9c7cc81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -147,6 +147,7 @@
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.dagger.KeyguardModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -265,6 +266,7 @@
     private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 17;
     private static final int SYSTEM_READY = 18;
     private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
+    private static final int BOOT_INTERACTOR = 20;
 
     /** Enum for reasons behind updating wakeAndUnlock state. */
     @Retention(RetentionPolicy.SOURCE)
@@ -1390,6 +1392,7 @@
     private final DozeParameters mDozeParameters;
     private final SelectedUserInteractor mSelectedUserInteractor;
     private final KeyguardInteractor mKeyguardInteractor;
+    private final KeyguardTransitionBootInteractor mTransitionBootInteractor;
     @VisibleForTesting
     protected FoldGracePeriodProvider mFoldGracePeriodProvider =
             new FoldGracePeriodProvider();
@@ -1484,6 +1487,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1524,6 +1528,7 @@
         mDozeParameters = dozeParameters;
         mSelectedUserInteractor = selectedUserInteractor;
         mKeyguardInteractor = keyguardInteractor;
+        mTransitionBootInteractor = transitionBootInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -1678,6 +1683,8 @@
             adjustStatusBarLocked();
             mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
 
+            mHandler.obtainMessage(BOOT_INTERACTOR).sendToTarget();
+
             final DreamViewModel dreamViewModel = mDreamViewModel.get();
             final CommunalTransitionViewModel communalViewModel =
                     mCommunalTransitionViewModel.get();
@@ -2705,11 +2712,19 @@
                     message = "SYSTEM_READY";
                     handleSystemReady();
                     break;
+                case BOOT_INTERACTOR:
+                    message = "BOOT_INTERACTOR";
+                    handleBootInteractor();
+                    break;
             }
             Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
         }
     };
 
+    private void handleBootInteractor() {
+        mTransitionBootInteractor.start();
+    }
+
     private void tryKeyguardDone() {
         if (DEBUG) {
             Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - "
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 8a3d017..d0a40ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -59,6 +59,7 @@
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
@@ -175,6 +176,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -225,6 +227,7 @@
                 wmLockscreenVisibilityManager,
                 selectedUserInteractor,
                 keyguardInteractor,
+                transitionBootInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 797a4ec..690ae71 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -23,6 +23,7 @@
 import android.annotation.SuppressLint
 import android.os.Trace
 import android.util.Log
+import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.withContext
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -95,7 +96,7 @@
      * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
      * seed the repository with the appropriate initial state.
      */
-    suspend fun emitInitialStepsFromOff(to: KeyguardState)
+    suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false)
 
     /**
      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
@@ -108,16 +109,14 @@
     suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     )
 }
 
 @SysUISingleton
 class KeyguardTransitionRepositoryImpl
 @Inject
-constructor(
-    @Main val mainDispatcher: CoroutineDispatcher,
-) : KeyguardTransitionRepository {
+constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionRepository {
     /**
      * Each transition between [KeyguardState]s will have an associated Flow. In order to collect
      * these events, clients should call [transition].
@@ -140,7 +139,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.OFF,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -159,12 +158,7 @@
         // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
         // start in.
         emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
     }
 
@@ -217,7 +211,7 @@
                         TransitionStep(
                             info,
                             (animation.animatedValue as Float),
-                            TransitionState.RUNNING
+                            TransitionState.RUNNING,
                         )
                     )
                 }
@@ -266,7 +260,7 @@
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         // There is no fairness guarantee with 'withContext', which means that transitions could
         // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
@@ -282,7 +276,7 @@
     private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         if (updateTransitionId != transitionId) {
             Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
@@ -303,34 +297,51 @@
         lastStep = nextStep
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
-        _currentTransitionInfo.value =
-            TransitionInfo(
-                ownerName = "KeyguardTransitionRepository(boot)",
-                from = KeyguardState.OFF,
-                to = to,
-                animator = null
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
+        val ownerName = "KeyguardTransitionRepository(boot)"
+        // Tests runs on testDispatcher, which is not the main thread, causing the animator thread
+        // check to fail
+        if (testSetup) {
+            _currentTransitionInfo.value =
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator = null,
+                )
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    0f,
+                    TransitionState.STARTED,
+                    ownerName = ownerName,
+                )
             )
 
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                0f,
-                TransitionState.STARTED,
-                ownerName = "KeyguardTransitionRepository(boot)",
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    1f,
+                    TransitionState.FINISHED,
+                    ownerName = ownerName,
+                )
             )
-        )
-
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                1f,
-                TransitionState.FINISHED,
-                ownerName = "KeyguardTransitionRepository(boot)",
-            ),
-        )
+        } else {
+            startTransition(
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator =
+                        ValueAnimator().apply {
+                            interpolator = Interpolators.LINEAR
+                            duration = 933L
+                        },
+                )
+            )
+        }
     }
 
     private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 0343786..840bc0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -106,7 +106,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -127,7 +127,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -147,7 +147,7 @@
                 communalSceneInteractor.changeScene(
                     newScene = CommunalScenes.Communal,
                     loggingReason = "occluded to hub",
-                    transitionKey = CommunalTransitionKeys.SimpleFade
+                    transitionKey = CommunalTransitionKeys.SimpleFade,
                 )
             } else {
                 startTransitionTo(KeyguardState.GLANCEABLE_HUB)
@@ -210,8 +210,9 @@
 
             duration =
                 when (toState) {
-                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.ALTERNATE_BOUNCER -> TO_ALTERNATE_BOUNCER_DURATION
                     KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -220,9 +221,10 @@
     companion object {
         const val TAG = "FromOccludedTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
-        val TO_LOCKSCREEN_DURATION = 933.milliseconds
-        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_ALTERNATE_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_AOD_DURATION = DEFAULT_DURATION
         val TO_DOZING_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_LOCKSCREEN_DURATION = 933.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index 18b1495..258232b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -28,6 +28,7 @@
 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
@@ -64,6 +65,7 @@
     alternateBouncerInteractor: AlternateBouncerInteractor,
     shadeInteractor: Lazy<ShadeInteractor>,
     keyguardInteractor: Lazy<KeyguardInteractor>,
+    sceneInteractor: Lazy<SceneInteractor>,
 ) {
     val dismissAction: Flow<DismissAction> = repository.dismissAction
 
@@ -125,7 +127,20 @@
 
     val executeDismissAction: Flow<() -> KeyguardDone> =
         merge(
-                finishedTransitionToGone,
+                if (SceneContainerFlag.isEnabled) {
+                    // Using currentScene instead of finishedTransitionToGone because of a race
+                    // condition that forms between finishedTransitionToGone and
+                    // isOnShadeWhileUnlocked where the latter emits false before the former emits
+                    // true, causing the merge to not emit until it's too late.
+                    sceneInteractor
+                        .get()
+                        .currentScene
+                        .map { it == Scenes.Gone }
+                        .distinctUntilChanged()
+                        .filter { it }
+                } else {
+                    finishedTransitionToGone
+                },
                 isOnShadeWhileUnlocked.filter { it }.map {},
                 dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction,
             )
@@ -135,10 +150,24 @@
 
     val resetDismissAction: Flow<Unit> =
         combine(
-                transitionInteractor.isFinishedIn(
-                    scene = Scenes.Gone,
-                    stateWithoutSceneContainer = GONE,
-                ),
+                if (SceneContainerFlag.isEnabled) {
+                    // Using currentScene instead of isFinishedIn because of a race condition that
+                    // forms between isFinishedIn(Gone) and isOnShadeWhileUnlocked where the latter
+                    // emits false before the former emits true, causing the evaluation of the
+                    // combine to come up with true, temporarily, before settling on false, which is
+                    // a valid final state. That causes an incorrect reset of the dismiss action to
+                    // occur before it gets executed.
+                    sceneInteractor
+                        .get()
+                        .currentScene
+                        .map { it == Scenes.Gone }
+                        .distinctUntilChanged()
+                } else {
+                    transitionInteractor.isFinishedIn(
+                        scene = Scenes.Gone,
+                        stateWithoutSceneContainer = GONE,
+                    )
+                },
                 transitionInteractor.isFinishedIn(
                     scene = Scenes.Bouncer,
                     stateWithoutSceneContainer = PRIMARY_BOUNCER,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
index b218300..89f636d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.util.Log
-import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -46,7 +45,7 @@
     val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
     val repository: KeyguardTransitionRepository,
-) : CoreStartable {
+) {
 
     /**
      * Whether the lockscreen should be showing when the device starts up for the first time. If not
@@ -60,14 +59,14 @@
         }
     }
 
-    override fun start() {
+    fun start() {
         scope.launch {
             if (internalTransitionInteractor.currentTransitionInfoInternal.value.from != OFF) {
                 Log.e(
                     "KeyguardTransitionInteractor",
                     "showLockscreenOnBoot emitted, but we've already " +
                         "transitioned to a state other than OFF. We'll respect that " +
-                        "transition, but this should not happen."
+                        "transition, but this should not happen.",
                 )
             } else {
                 if (SceneContainerFlag.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index 25b8fd3..b715333 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -27,7 +27,6 @@
 constructor(
     private val interactors: Set<TransitionInteractor>,
     private val auditLogger: KeyguardTransitionAuditLogger,
-    private val bootInteractor: KeyguardTransitionBootInteractor,
     private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor,
     private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor,
 ) : CoreStartable {
@@ -54,7 +53,6 @@
             it.start()
         }
         auditLogger.start()
-        bootInteractor.start()
         statusBarDisableFlagsInteractor.start()
         keyguardStateCallbackInteractor.start()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index f1b9cba..00aa44f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -47,56 +47,53 @@
         constraintLayout.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch("$TAG#viewModel.blueprint") {
-                    viewModel.blueprint
-                        .pairwise(
-                            null as KeyguardBlueprint?,
-                        )
-                        .collect { (prevBlueprint, blueprint) ->
-                            val config = Config.DEFAULT
-                            val transition =
-                                if (
-                                    !KeyguardBottomAreaRefactor.isEnabled &&
-                                        prevBlueprint != null &&
-                                        prevBlueprint != blueprint
-                                ) {
-                                    BaseBlueprintTransition(clockViewModel)
-                                        .addTransition(
-                                            IntraBlueprintTransition(
-                                                config,
-                                                clockViewModel,
-                                                smartspaceViewModel
-                                            )
+                    viewModel.blueprint.pairwise(null as KeyguardBlueprint?).collect {
+                        (prevBlueprint, blueprint) ->
+                        val config = Config.DEFAULT
+                        val transition =
+                            if (
+                                !KeyguardBottomAreaRefactor.isEnabled &&
+                                    prevBlueprint != null &&
+                                    prevBlueprint != blueprint
+                            ) {
+                                BaseBlueprintTransition(clockViewModel)
+                                    .addTransition(
+                                        IntraBlueprintTransition(
+                                            config,
+                                            clockViewModel,
+                                            smartspaceViewModel,
                                         )
-                                } else {
-                                    IntraBlueprintTransition(
-                                        config,
-                                        clockViewModel,
-                                        smartspaceViewModel
                                     )
+                            } else {
+                                IntraBlueprintTransition(
+                                    config,
+                                    clockViewModel,
+                                    smartspaceViewModel,
+                                )
+                            }
+
+                        viewModel.runTransition(constraintLayout, transition, config) {
+                            // Replace sections from the previous blueprint with the new ones
+                            blueprint.replaceViews(
+                                constraintLayout,
+                                prevBlueprint,
+                                config.rebuildSections,
+                            )
+
+                            val cs =
+                                ConstraintSet().apply {
+                                    clone(constraintLayout)
+                                    val emptyLayout = ConstraintSet.Layout()
+                                    knownIds.forEach {
+                                        getConstraint(it).layout.copyFrom(emptyLayout)
+                                    }
+                                    blueprint.applyConstraints(this)
                                 }
 
-                            viewModel.runTransition(constraintLayout, transition, config) {
-                                // Replace sections from the previous blueprint with the new ones
-                                blueprint.replaceViews(
-                                    constraintLayout,
-                                    prevBlueprint,
-                                    config.rebuildSections
-                                )
-
-                                val cs =
-                                    ConstraintSet().apply {
-                                        clone(constraintLayout)
-                                        val emptyLayout = ConstraintSet.Layout()
-                                        knownIds.forEach {
-                                            getConstraint(it).layout.copyFrom(emptyLayout)
-                                        }
-                                        blueprint.applyConstraints(this)
-                                    }
-
-                                logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
-                                cs.applyTo(constraintLayout)
-                            }
+                            logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
+                            cs.applyTo(constraintLayout)
                         }
+                    }
                 }
 
                 launch("$TAG#viewModel.refreshTransition") {
@@ -105,7 +102,8 @@
 
                         viewModel.runTransition(
                             constraintLayout,
-                            IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel),
+                            clockViewModel,
+                            smartspaceViewModel,
                             config,
                         ) {
                             blueprint.rebuildViews(constraintLayout, config.rebuildSections)
@@ -126,7 +124,7 @@
 
     private fun logAlphaVisibilityScaleOfAppliedConstraintSet(
         cs: ConstraintSet,
-        viewModel: KeyguardClockViewModel
+        viewModel: KeyguardClockViewModel,
     ) {
         val currentClock = viewModel.currentClock.value
         if (!DEBUG || currentClock == null) return
@@ -137,19 +135,19 @@
             TAG,
             "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
                 "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha} " +
-                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} "
+                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} ",
         )
         Log.i(
             TAG,
             "applyCsToLargeClock: vis=${cs.getVisibility(largeClockViewId)} " +
                 "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha} " +
                 "scale=${cs.getConstraint(largeClockViewId).transform.scaleX} " +
-                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} "
+                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} ",
         )
         Log.i(
             TAG,
             "applyCsToSmartspaceDate: vis=${cs.getVisibility(smartspaceDateId)} " +
-                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}"
+                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}",
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index aa0a9d9..9a55f7b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -29,18 +29,18 @@
     smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) : TransitionSet() {
 
-    enum class Type(
-        val priority: Int,
-        val animateNotifChanges: Boolean,
-    ) {
+    enum class Type(val priority: Int, val animateNotifChanges: Boolean) {
         ClockSize(100, true),
         ClockCenter(99, false),
         DefaultClockStepping(98, false),
-        SmartspaceVisibility(2, true),
-        DefaultTransition(1, false),
+        SmartspaceVisibility(3, true),
+        DefaultTransition(2, false),
         // When transition between blueprint, we don't need any duration or interpolator but we need
         // all elements go to correct state
-        NoTransition(0, false),
+        NoTransition(1, false),
+        // Similar to NoTransition, except also does not explicitly update any alpha. Used in
+        // OFF->LOCKSCREEN transition
+        Init(0, false),
     }
 
     data class Config(
@@ -57,6 +57,7 @@
     init {
         ordering = ORDERING_TOGETHER
         when (config.type) {
+            Type.Init -> {}
             Type.NoTransition -> {}
             Type.DefaultClockStepping ->
                 addTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index ff84826..a1c963b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -53,14 +53,11 @@
 internal fun ConstraintSet.setVisibility(views: Iterable<View>, visibility: Int) =
     views.forEach { view -> this.setVisibility(view.id, visibility) }
 
-internal fun ConstraintSet.setAlpha(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setAlpha(view.id, alpha) }
+internal fun ConstraintSet.setScaleX(views: Iterable<View>, scaleX: Float) =
+    views.forEach { view -> this.setScaleX(view.id, scaleX) }
 
-internal fun ConstraintSet.setScaleX(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleX(view.id, alpha) }
-
-internal fun ConstraintSet.setScaleY(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleY(view.id, alpha) }
+internal fun ConstraintSet.setScaleY(views: Iterable<View>, scaleY: Float) =
+    views.forEach { view -> this.setScaleY(view.id, scaleY) }
 
 @SysUISingleton
 class ClockSection
@@ -126,8 +123,6 @@
         return constraintSet.apply {
             setVisibility(getTargetClockFace(clock).views, VISIBLE)
             setVisibility(getNonTargetClockFace(clock).views, GONE)
-            setAlpha(getTargetClockFace(clock).views, 1F)
-            setAlpha(getNonTargetClockFace(clock).views, 0F)
             if (!keyguardClockViewModel.isLargeClockVisible.value) {
                 connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
index 3f2ef29..c49e783 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
@@ -28,22 +28,22 @@
 import kotlinx.coroutines.flow.Flow
 
 /**
- * Breaks down ALTERNATE_BOUNCER->GONE transition into discrete steps for corresponding views to
+ * Breaks down ALTERNATE_BOUNCER->OCCLUDED transition into discrete steps for corresponding views to
  * consume.
  */
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class AlternateBouncerToOccludedTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_OCCLUDED_DURATION,
             edge = Edge.create(from = ALTERNATE_BOUNCER, to = OCCLUDED),
         )
 
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index a021de4..ca1a800 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -56,6 +56,7 @@
     occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     primaryBouncerToDozingTransitionViewModel: PrimaryBouncerToDozingTransitionViewModel,
     primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
@@ -67,14 +68,14 @@
                     .map {
                         Utils.getColorAttrDefaultColor(
                             context,
-                            com.android.internal.R.attr.colorSurface
+                            com.android.internal.R.attr.colorSurface,
                         )
                     }
                     .onStart {
                         emit(
                             Utils.getColorAttrDefaultColor(
                                 context,
-                                com.android.internal.R.attr.colorSurface
+                                com.android.internal.R.attr.colorSurface,
                             )
                         )
                     }
@@ -86,23 +87,23 @@
         deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
             if (useBackground) {
                 setOf(
-                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToLockscreenTransitionViewModel
-                            .deviceEntryBackgroundViewAlpha,
+                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         occludedToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        offToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                     )
                     .merge()
                     .onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index 4cf3c4e..1289036 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -24,6 +24,9 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
@@ -37,6 +40,7 @@
 constructor(
     @Main private val handler: Handler,
     private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -49,12 +53,12 @@
     private val transitionListener =
         object : Transition.TransitionListener {
             override fun onTransitionCancel(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
+                if (DEBUG) Log.w(TAG, "onTransitionCancel: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
             override fun onTransitionEnd(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+                if (DEBUG) Log.i(TAG, "onTransitionEnd: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
@@ -86,6 +90,28 @@
 
     fun runTransition(
         constraintLayout: ConstraintLayout,
+        clockViewModel: KeyguardClockViewModel,
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
+        config: Config,
+        apply: () -> Unit,
+    ) {
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
+        runTransition(
+            constraintLayout,
+            IntraBlueprintTransition(newConfig, clockViewModel, smartspaceViewModel),
+            config,
+            apply,
+        )
+    }
+
+    fun runTransition(
+        constraintLayout: ConstraintLayout,
         transition: Transition,
         config: Config,
         apply: () -> Unit,
@@ -103,21 +129,29 @@
             return
         }
 
+        // Don't allow transitions with animations while in OFF state
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
         if (DEBUG) {
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config",
+                    "currentPriority=$currentPriority; config=$newConfig",
             )
         }
 
         // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
         // the running set until the copy is started by the handler.
-        updateTransitions(TransitionData(config)) { add(transition) }
+        updateTransitions(TransitionData(newConfig)) { add(transition) }
         transition.addListener(transitionListener)
 
         handler.post {
-            if (config.terminatePrevious) {
+            if (newConfig.terminatePrevious) {
                 TransitionManager.endTransitions(constraintLayout)
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 10a2e5c..3705c2c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -20,7 +20,6 @@
 import android.graphics.Point
 import android.util.MathUtils
 import android.view.View.VISIBLE
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +34,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
@@ -88,6 +88,8 @@
         AlternateBouncerToGoneTransitionViewModel,
     private val alternateBouncerToLockscreenTransitionViewModel:
         AlternateBouncerToLockscreenTransitionViewModel,
+    private val alternateBouncerToOccludedTransitionViewModel:
+        AlternateBouncerToOccludedTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -112,9 +114,12 @@
     private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
     private val lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel,
+    private val occludedToAlternateBouncerTransitionViewModel:
+        OccludedToAlternateBouncerTransitionViewModel,
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
@@ -201,6 +206,10 @@
             notificationShadeWindowModel.isKeyguardOccluded,
             communalInteractor.isIdleOnCommunal,
             keyguardTransitionInteractor
+                .transitionValue(OFF)
+                .map { it > 1f - offToLockscreenTransitionViewModel.alphaStartAt }
+                .onStart { emit(false) },
+            keyguardTransitionInteractor
                 .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
                 .map { it == 1f }
                 .onStart { emit(false) },
@@ -227,6 +236,7 @@
                         alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        alternateBouncerToOccludedTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
@@ -249,14 +259,16 @@
                         lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
                         lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+                        occludedToAlternateBouncerTransitionViewModel.lockscreenAlpha,
                         occludedToAodTransitionViewModel.lockscreenAlpha,
                         occludedToDozingTransitionViewModel.lockscreenAlpha,
                         occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+                        offToLockscreenTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                     )
-                    .onStart { emit(1f) },
+                    .onStart { emit(0f) },
             ) { hideKeyguard, alpha ->
                 if (hideKeyguard) {
                     0f
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 8d9ccef..88e8968 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -52,18 +52,26 @@
 
     /** Lockscreen views alpha */
     val lockscreenAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1f - it },
-            name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     val shortcutsAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1 - it },
-            onFinish = { 0f },
-            onCancel = { 1f },
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onFinish = { 0f },
+                    onCancel = { 1f },
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
index 3b266f9..6f29004 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
@@ -18,20 +18,18 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
-import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,7 +60,7 @@
                 ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
                     buildList {
                             if (isCommunalAvailable) {
-                                add(Swipe.Left to Scenes.Communal)
+                                add(Swipe.Start to Scenes.Communal)
                             }
 
                             add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer)
@@ -81,45 +79,6 @@
             .collect { setActions(it) }
     }
 
-    private fun singleShadeActions(): Array<Pair<UserAction, UserActionResult>> {
-        return arrayOf(
-            // Swiping down, not from the edge, always goes to shade.
-            Swipe.Down to Scenes.Shade,
-            swipeDown(pointerCount = 2) to Scenes.Shade,
-            // Swiping down from the top edge goes to QS.
-            swipeDownFromTop(pointerCount = 1) to Scenes.QuickSettings,
-            swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings,
-        )
-    }
-
-    private fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
-        val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade)
-        return arrayOf(
-            // Swiping down, not from the edge, always goes to shade.
-            Swipe.Down to splitShadeSceneKey,
-            swipeDown(pointerCount = 2) to splitShadeSceneKey,
-            // Swiping down from the top edge goes to QS.
-            swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey,
-            swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey,
-        )
-    }
-
-    private fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
-        return arrayOf(
-            Swipe.Down to Overlays.NotificationsShade,
-            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
-                Overlays.QuickSettingsShade,
-        )
-    }
-
-    private fun swipeDownFromTop(pointerCount: Int): Swipe {
-        return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
-    }
-
-    private fun swipeDown(pointerCount: Int): Swipe {
-        return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
-    }
-
     @AssistedFactory
     interface Factory {
         fun create(): LockscreenUserActionsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..5bfcccb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_ALTERNATE_BOUNCER_DURATION
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down OCCLUDED->ALTERNATE_BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class OccludedToAlternateBouncerTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = TO_ALTERNATE_BOUNCER_DURATION,
+            edge = Edge.create(from = OCCLUDED, to = ALTERNATE_BOUNCER),
+        )
+
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 1eecbd5..b4acce6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -29,23 +30,29 @@
 @SysUISingleton
 class OffToLockscreenTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+
+    private val startTime = 300.milliseconds
+    private val alphaDuration = 633.milliseconds
+    val alphaStartAt = startTime / (alphaDuration + startTime)
 
     private val transitionAnimation =
         animationFlow.setup(
-            duration = 250.milliseconds,
+            duration = startTime + alphaDuration,
             edge = Edge.create(from = OFF, to = LOCKSCREEN),
         )
 
-    val shortcutsAlpha: Flow<Float> =
+    val lockscreenAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
+            startTime = startTime,
+            duration = alphaDuration,
+            interpolator = EMPHASIZED_ACCELERATE,
             onStep = { it },
-            onCancel = { 0f },
         )
 
-    override val deviceEntryParentViewAlpha: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(1f)
+    val shortcutsAlpha: Flow<Float> = lockscreenAlpha
+
+    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 84aae65..222d783 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -111,7 +111,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataManager"
@@ -136,7 +136,7 @@
         active = true,
         resumeAction = null,
         instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID
+        appUid = Process.INVALID_UID,
     )
 
 internal val EMPTY_SMARTSPACE_MEDIA_DATA =
@@ -163,7 +163,7 @@
         Settings.Secure.getInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            1
+            1,
         )
     return Utils.useQsMediaPlayer(context) && flag > 0
 }
@@ -217,7 +217,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -387,7 +387,7 @@
                 uiExecutor,
                 SmartspaceSession.OnTargetsAvailableListener { targets ->
                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                }
+                },
             )
         }
         smartspaceSession?.let { it.requestSmartspaceUpdate() }
@@ -398,12 +398,12 @@
                     if (!allowMediaRecommendations) {
                         dismissSmartspaceRecommendation(
                             key = smartspaceMediaData.targetId,
-                            delay = 0L
+                            delay = 0L,
                         )
                     }
                 }
             },
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
         )
     }
 
@@ -461,7 +461,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaEntries.containsKey(packageName)) {
@@ -497,7 +497,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -509,7 +509,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -609,14 +609,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != currentEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -722,30 +722,32 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     private fun updateState(key: String, state: PlaybackState) {
         mediaEntries.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
-
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+            backgroundExecutor.execute {
+                val token = it.token
+                if (token == null) {
+                    if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                    return@execute
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+                val actions =
+                    createActionsFromState(
+                        it.packageName,
+                        mediaControllerFactory.create(it.token),
+                        UserHandle(it.userId),
+                    )
+
+                // Control buttons
+                // If flag is enabled and controller has a PlaybackState,
+                // create actions from session info
+                // otherwise, no need to update semantic actions.
+                val data =
+                    if (actions != null) {
+                        it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                    } else {
+                        it.copy(isPlaying = isPlayingState(state.state))
+                    }
+                if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                foregroundExecutor.execute { onMediaDataLoaded(key, key, data) }
+            }
         }
     }
 
@@ -773,7 +775,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key = key, userInitiated = userInitiated) },
-            delay
+            delay,
         )
         return existed
     }
@@ -793,12 +795,12 @@
             smartspaceMediaData =
                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                     targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId
+                    instanceId = smartspaceMediaData.instanceId,
                 )
         }
         foregroundExecutor.executeDelayed(
             { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
-            delay
+            delay,
         )
     }
 
@@ -826,7 +828,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -843,7 +845,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -882,7 +884,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -895,7 +897,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -966,7 +968,7 @@
                     appUid = appUid,
                     isExplicit = isExplicit,
                     resumeProgress = progress,
-                )
+                ),
             )
         }
     }
@@ -981,7 +983,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -993,7 +995,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -1057,7 +1059,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1073,7 +1075,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1160,7 +1162,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1169,7 +1171,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaEntries.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1207,7 +1209,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1245,7 +1247,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1282,7 +1284,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1307,7 +1309,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1371,10 +1373,7 @@
                 // There should NOT be more than 1 Smartspace media update. When it happens, it
                 // indicates a bad state or an error. Reset the status accordingly.
                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
-                notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId,
-                    immediately = false,
-                )
+                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
             }
         }
@@ -1420,7 +1419,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1445,7 +1444,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index f2825d0..4f97913 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.domain.pipeline
 
+import android.annotation.WorkerThread
 import android.app.ActivityOptions
 import android.app.BroadcastOptions
 import android.app.Notification
@@ -50,6 +51,7 @@
  * @return a Pair consisting of a list of media actions, and a list of ints representing which of
  *   those actions should be shown in the compact player
  */
+@WorkerThread
 fun createActionsFromState(
     context: Context,
     packageName: String,
@@ -69,7 +71,7 @@
                 context.getString(R.string.controls_media_button_connecting),
                 context.getDrawable(R.drawable.ic_media_connecting_container),
                 // Specify a rebind id to prevent the spinner from restarting on later binds.
-                com.android.internal.R.drawable.progress_small_material
+                com.android.internal.R.drawable.progress_small_material,
             )
         } else if (isPlayingState(state.state)) {
             getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE)
@@ -128,7 +130,7 @@
         nextCustomAction(),
         nextCustomAction(),
         reserveNext,
-        reservePrev
+        reservePrev,
     )
 }
 
@@ -146,7 +148,7 @@
     context: Context,
     controller: MediaController,
     stateActions: Long,
-    @PlaybackState.Actions action: Long
+    @PlaybackState.Actions action: Long,
 ): MediaAction? {
     if (!includesAction(stateActions, action)) {
         return null
@@ -158,7 +160,7 @@
                 context.getDrawable(R.drawable.ic_media_play),
                 { controller.transportControls.play() },
                 context.getString(R.string.controls_media_button_play),
-                context.getDrawable(R.drawable.ic_media_play_container)
+                context.getDrawable(R.drawable.ic_media_play_container),
             )
         }
         PlaybackState.ACTION_PAUSE -> {
@@ -166,7 +168,7 @@
                 context.getDrawable(R.drawable.ic_media_pause),
                 { controller.transportControls.pause() },
                 context.getString(R.string.controls_media_button_pause),
-                context.getDrawable(R.drawable.ic_media_pause_container)
+                context.getDrawable(R.drawable.ic_media_pause_container),
             )
         }
         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
@@ -174,7 +176,7 @@
                 MediaControlDrawables.getPrevIcon(context),
                 { controller.transportControls.skipToPrevious() },
                 context.getString(R.string.controls_media_button_prev),
-                null
+                null,
             )
         }
         PlaybackState.ACTION_SKIP_TO_NEXT -> {
@@ -182,7 +184,7 @@
                 MediaControlDrawables.getNextIcon(context),
                 { controller.transportControls.skipToNext() },
                 context.getString(R.string.controls_media_button_next),
-                null
+                null,
             )
         }
         else -> null
@@ -194,13 +196,13 @@
     context: Context,
     packageName: String,
     controller: MediaController,
-    customAction: PlaybackState.CustomAction
+    customAction: PlaybackState.CustomAction,
 ): MediaAction {
     return MediaAction(
         Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
         { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
         customAction.name,
-        null
+        null,
     )
 }
 
@@ -218,7 +220,7 @@
 /** Generate action buttons based on notification actions */
 fun createActionsFromNotification(
     context: Context,
-    sbn: StatusBarNotification
+    sbn: StatusBarNotification,
 ): Pair<List<MediaNotificationAction>, List<Int>> {
     val notif = sbn.notification
     val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
@@ -229,7 +231,7 @@
     if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
         Log.e(
             TAG,
-            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS"
+            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS",
         )
         actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
     }
@@ -239,7 +241,7 @@
             Log.w(
                 TAG,
                 "Too many notification actions for ${sbn.key}, " +
-                    "limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    "limiting to first $MAX_NOTIFICATION_ACTIONS",
             )
         }
 
@@ -253,7 +255,7 @@
             val themeText =
                 com.android.settingslib.Utils.getColorAttr(
                         context,
-                        com.android.internal.R.attr.textColorPrimary
+                        com.android.internal.R.attr.textColorPrimary,
                     )
                     .defaultColor
 
@@ -271,7 +273,7 @@
                     action.isAuthenticationRequired,
                     action.actionIntent,
                     mediaActionIcon,
-                    action.title
+                    action.title,
                 )
             actionIcons.add(mediaAction)
         }
@@ -288,7 +290,7 @@
  */
 fun getNotificationActions(
     actions: List<MediaNotificationAction>,
-    activityStarter: ActivityStarter
+    activityStarter: ActivityStarter,
 ): List<MediaAction> {
     return actions.map { action ->
         val runnable =
@@ -303,7 +305,7 @@
                             activityStarter.dismissKeyguardThenExecute(
                                 { sendPendingIntent(action.actionIntent) },
                                 {},
-                                true
+                                true,
                             )
                         else -> sendPendingIntent(actionIntent)
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 5f0a9f8..fd7b6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -119,7 +119,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataProcessor"
@@ -177,7 +177,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -365,7 +365,7 @@
                 secureSettings.getBoolForUser(
                     Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
                     true,
-                    UserHandle.USER_CURRENT
+                    UserHandle.USER_CURRENT,
                 )
 
             useQsMediaPlayer && flag
@@ -386,7 +386,7 @@
                 if (!allowMediaRecommendations) {
                     dismissSmartspaceRecommendation(
                         key = mediaDataRepository.smartspaceMediaData.value.targetId,
-                        delay = 0L
+                        delay = 0L,
                     )
                 }
             }
@@ -413,7 +413,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) {
@@ -449,7 +449,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -461,7 +461,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -582,30 +582,37 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     internal fun updateState(key: String, state: PlaybackState) {
         mediaDataRepository.mediaEntries.value.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
+            applicationScope.launch {
+                withContext(backgroundDispatcher) {
+                    val token = it.token
+                    if (token == null) {
+                        if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                        return@withContext
+                    }
+                    val actions =
+                        createActionsFromState(
+                            it.packageName,
+                            mediaControllerFactory.create(it.token),
+                            UserHandle(it.userId),
+                        )
 
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+                    // Control buttons
+                    // If flag is enabled and controller has a PlaybackState,
+                    // create actions from session info
+                    // otherwise, no need to update semantic actions.
+                    val data =
+                        if (actions != null) {
+                            it.copy(
+                                semanticActions = actions,
+                                isPlaying = isPlayingState(state.state),
+                            )
+                        } else {
+                            it.copy(isPlaying = isPlayingState(state.state))
+                        }
+                    if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                    withContext(mainDispatcher) { onMediaDataLoaded(key, key, data) }
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+            }
         }
     }
 
@@ -633,7 +640,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key, userInitiated = userInitiated) },
-            delayMs
+            delayMs,
         )
         return existed
     }
@@ -657,7 +664,7 @@
         if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
             foregroundExecutor.executeDelayed(
                 { notifySmartspaceMediaDataRemoved(key, immediately = true) },
-                delay
+                delay,
             )
         }
     }
@@ -677,7 +684,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -694,7 +701,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -733,7 +740,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -746,7 +753,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -818,7 +825,7 @@
                     isExplicit = isExplicit,
                     resumeProgress = progress,
                     smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
-                )
+                ),
             )
         }
     }
@@ -887,14 +894,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != oldEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -911,7 +918,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -923,7 +930,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -987,7 +994,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1003,7 +1010,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1093,7 +1100,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1102,7 +1109,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaDataRepository.mediaEntries.value.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1151,7 +1158,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1189,7 +1196,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1226,7 +1233,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1251,7 +1258,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1291,7 +1298,7 @@
                 } else {
                     notifySmartspaceMediaDataRemoved(
                         smartspaceMediaData.targetId,
-                        immediately = false
+                        immediately = false,
                     )
                     mediaDataRepository.setRecommendation(
                         SmartspaceMediaData(
@@ -1362,7 +1369,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1387,7 +1394,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
@@ -1513,7 +1520,7 @@
             data: MediaData,
             immediately: Boolean = true,
             receivedSmartspaceCardLatency: Int = 0,
-            isSsReactivated: Boolean = false
+            isSsReactivated: Boolean = false,
         ) {}
 
         /**
@@ -1526,7 +1533,7 @@
         fun onSmartspaceMediaDataLoaded(
             key: String,
             data: SmartspaceMediaData,
-            shouldPrioritize: Boolean = false
+            shouldPrioritize: Boolean = false,
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index a0fb0bf2..72650ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.annotation.WorkerThread
 import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
@@ -41,6 +42,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -137,7 +139,7 @@
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
     @Main private val mainDispatcher: CoroutineDispatcher,
-    @Main executor: DelayableExecutor,
+    @Main private val uiExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val mediaManager: MediaDataManager,
@@ -227,7 +229,7 @@
     private var carouselLocale: Locale? = null
 
     private val animationScaleObserver: ContentObserver =
-        object : ContentObserver(executor, 0) {
+        object : ContentObserver(uiExecutor, 0) {
             override fun onChange(selfChange: Boolean) {
                 if (!SceneContainerFlag.isEnabled) {
                     MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
@@ -350,7 +352,7 @@
             MediaCarouselScrollHandler(
                 mediaCarousel,
                 pageIndicator,
-                executor,
+                uiExecutor,
                 this::onSwipeToDismiss,
                 this::updatePageIndicatorLocation,
                 this::updateSeekbarListening,
@@ -458,7 +460,17 @@
                     isSsReactivated: Boolean,
                 ) {
                     debugLogger.logMediaLoaded(key, data.active)
-                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
+                    val onUiExecutionEnd =
+                        if (mediaControlsUmoInflationInBackground()) {
+                            Runnable {
+                                if (immediately) {
+                                    updateHostVisibility()
+                                }
+                            }
+                        } else {
+                            null
+                        }
+                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) {
                         // Log card received if a new resumable media card is added
                         MediaPlayerData.getMediaPlayer(key)?.let {
                             logSmartspaceCardReported(
@@ -980,6 +992,7 @@
         oldKey: String?,
         data: MediaData,
         isSsReactivated: Boolean,
+        onUiExecutionEnd: Runnable? = null,
     ): Boolean =
         traceSection("MediaCarouselController#addOrUpdatePlayer") {
             MediaPlayerData.moveIfExists(oldKey, key)
@@ -987,76 +1000,119 @@
             val curVisibleMediaKey =
                 MediaPlayerData.visiblePlayerKeys()
                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-            if (existingPlayer == null) {
-                val newPlayer = mediaControlPanelFactory.get()
-                if (SceneContainerFlag.isEnabled) {
-                    newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx
-                    newPlayer.mediaViewController.heightInSceneContainerPx =
-                        heightInSceneContainerPx
-                }
-                newPlayer.attachPlayer(
-                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
-                )
-                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-                val lp =
-                    LinearLayout.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                    )
-                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-                newPlayer.bindPlayer(data, key)
-                newPlayer.setListening(
-                    mediaCarouselScrollHandler.visibleToUser && currentlyExpanded
-                )
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    newPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
-                // Media data added from a recommendation card should starts playing.
-                if (
-                    (shouldScrollToKey && data.isPlaying == true) ||
-                        (!shouldScrollToKey && data.active)
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+            if (mediaControlsUmoInflationInBackground()) {
+                if (existingPlayer == null) {
+                    bgExecutor.execute {
+                        val mediaViewHolder = createMediaViewHolderInBg()
+                        // Add the new player in the main thread.
+                        uiExecutor.execute {
+                            setupNewPlayer(
+                                key,
+                                data,
+                                isSsReactivated,
+                                curVisibleMediaKey,
+                                mediaViewHolder,
+                            )
+                            updatePageIndicator()
+                            mediaCarouselScrollHandler.onPlayersChanged()
+                            mediaFrame.requiresRemeasuring = true
+                            onUiExecutionEnd?.run()
+                        }
+                    }
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
+                    updatePageIndicator()
+                    mediaCarouselScrollHandler.onPlayersChanged()
+                    mediaFrame.requiresRemeasuring = true
+                    onUiExecutionEnd?.run()
                 }
             } else {
-                existingPlayer.bindPlayer(data, key)
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    existingPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
-                // In case of recommendations hits.
-                // Check the playing status of media player and the package name.
-                // To make sure we scroll to the right app's media player.
-                if (
-                    isReorderingAllowed ||
-                        shouldScrollToKey &&
-                            data.isPlaying == true &&
-                            packageName == data.packageName
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+                if (existingPlayer == null) {
+                    val mediaViewHolder =
+                        MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+                    setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder)
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
                 }
+                updatePageIndicator()
+                mediaCarouselScrollHandler.onPlayersChanged()
+                mediaFrame.requiresRemeasuring = true
+                onUiExecutionEnd?.run()
             }
-            updatePageIndicator()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            mediaFrame.requiresRemeasuring = true
             return existingPlayer == null
         }
 
+    private fun updatePlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        existingPlayer: MediaControlPanel,
+    ) {
+        existingPlayer.bindPlayer(data, key)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            existingPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
+        // In case of recommendations hits.
+        // Check the playing status of media player and the package name.
+        // To make sure we scroll to the right app's media player.
+        if (
+            isReorderingAllowed ||
+                shouldScrollToKey && data.isPlaying == true && packageName == data.packageName
+        ) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    private fun setupNewPlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        mediaViewHolder: MediaViewHolder,
+    ) {
+        val newPlayer = mediaControlPanelFactory.get()
+        newPlayer.attachPlayer(mediaViewHolder)
+        newPlayer.mediaViewController.sizeChangedListener =
+            this@MediaCarouselController::updateCarouselDimensions
+        val lp =
+            LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+            )
+        newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+        newPlayer.bindPlayer(data, key)
+        newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            newPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
+        // Media data added from a recommendation card should starts playing.
+        if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    @WorkerThread
+    private fun createMediaViewHolderInBg(): MediaViewHolder {
+        return MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+    }
+
     private fun addSmartspaceMediaRecommendations(
         key: String,
         data: SmartspaceMediaData,
@@ -1173,8 +1229,16 @@
         val previousVisibleKey =
             MediaPlayerData.visiblePlayerKeys()
                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+        val onUiExecutionEnd = Runnable {
+            if (recreateMedia) {
+                reorderAllPlayers(previousVisibleKey)
+            }
+        }
 
-        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
+        val mediaDataList = MediaPlayerData.mediaData()
+        // Do not loop through the original list of media data because the re-addition of media data
+        // is being executed in background thread.
+        mediaDataList.forEach { (key, data, isSsMediaRec) ->
             if (isSsMediaRec) {
                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
@@ -1185,6 +1249,7 @@
                         MediaPlayerData.shouldPrioritizeSs,
                     )
                 }
+                onUiExecutionEnd.run()
             } else {
                 val isSsReactivated = MediaPlayerData.isSsReactivated(key)
                 if (recreateMedia) {
@@ -1195,11 +1260,9 @@
                     oldKey = null,
                     data = data,
                     isSsReactivated = isSsReactivated,
+                    onUiExecutionEnd = onUiExecutionEnd,
                 )
             }
-            if (recreateMedia) {
-                reorderAllPlayers(previousVisibleKey)
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
index 8660d12..782da4b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.ui.controller
 
 import com.android.app.tracing.traceSection
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.util.animation.MeasurementOutput
@@ -71,23 +72,34 @@
      */
     fun updateCarouselDimensions(
         @MediaLocation location: Int,
-        hostState: MediaHostState
+        hostState: MediaHostState,
     ): MeasurementOutput =
         traceSection("MediaHostStatesManager#updateCarouselDimensions") {
             val result = MeasurementOutput(0, 0)
+            var changed = false
             for (controller in controllers) {
                 val measurement = controller.getMeasurementsForState(hostState)
                 measurement?.let {
                     if (it.measuredHeight > result.measuredHeight) {
                         result.measuredHeight = it.measuredHeight
+                        changed = true
                     }
                     if (it.measuredWidth > result.measuredWidth) {
                         result.measuredWidth = it.measuredWidth
+                        changed = true
                     }
                 }
             }
-            carouselSizes[location] = result
-            return result
+            if (mediaControlsUmoInflationInBackground()) {
+                // Set carousel size if result measurements changed. This avoids setting carousel
+                // size when this method gets called before the addition of media view controllers
+                if (!carouselSizes.contains(location) || changed) {
+                    carouselSizes[location] = result
+                }
+            } else {
+                carouselSizes[location] = result
+            }
+            return carouselSizes[location] ?: result
         }
 
     /** Add a callback to be called when a MediaState has updated */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
index 09a6181..5ddc347 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
@@ -20,6 +20,7 @@
 import android.util.ArraySet
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -91,8 +92,10 @@
                 data: MediaData,
                 immediately: Boolean,
                 receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
+                isSsReactivated: Boolean,
             ) {
+                if (mediaControlsUmoInflationInBackground()) return
+
                 if (immediately) {
                     updateViewVisibility()
                 }
@@ -101,7 +104,7 @@
             override fun onSmartspaceMediaDataLoaded(
                 key: String,
                 data: SmartspaceMediaData,
-                shouldPrioritize: Boolean
+                shouldPrioritize: Boolean,
             ) {
                 updateViewVisibility()
             }
@@ -171,7 +174,7 @@
                         input.widthMeasureSpec =
                             View.MeasureSpec.makeMeasureSpec(
                                 View.MeasureSpec.getSize(input.widthMeasureSpec),
-                                View.MeasureSpec.EXACTLY
+                                View.MeasureSpec.EXACTLY,
                             )
                     }
                     // This will trigger a state change that ensures that we now have a state
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index ba0d938..66ac01a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -180,6 +180,7 @@
         qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
         qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
         setListenerCollections()
+        lifecycleScope.launch { viewModel.activate() }
     }
 
     override fun onCreateView(
@@ -331,7 +332,7 @@
     }
 
     override fun setOverscrolling(overscrolling: Boolean) {
-        viewModel.stackScrollerOverscrollingValue = overscrolling
+        viewModel.isStackScrollerOverscrolling = overscrolling
     }
 
     override fun setExpanded(qsExpanded: Boolean) {
@@ -410,11 +411,11 @@
         qsTransitionFraction: Float,
         qsSquishinessFraction: Float,
     ) {
-        super.setTransitionToFullShadeProgress(
-            isTransitioningToFullShade,
-            qsTransitionFraction,
-            qsSquishinessFraction,
-        )
+        viewModel.isTransitioningToFullShade = isTransitioningToFullShade
+        viewModel.lockscreenToShadeProgressValue = qsTransitionFraction
+        if (isTransitioningToFullShade) {
+            viewModel.squishinessFractionValue = qsSquishinessFraction
+        }
     }
 
     override fun setFancyClipping(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 7300ee1..2d4e358 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -24,16 +24,19 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
-import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printSection
@@ -50,6 +53,7 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
@@ -61,13 +65,14 @@
     private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
     private val footerActionsController: FooterActionsController,
     private val sysuiStatusBarStateController: SysuiStatusBarStateController,
-    private val keyguardBypassController: KeyguardBypassController,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
     private val disableFlagsRepository: DisableFlagsRepository,
     private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
     private val configurationInteractor: ConfigurationInteractor,
     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
+    private val squishinessInteractor: TileSquishinessInteractor,
     @Assisted private val lifecycleScope: LifecycleCoroutineScope,
-) : Dumpable {
+) : Dumpable, ExclusiveActivatable() {
     val footerActionsViewModel =
         footerActionsViewModelFactory.create(lifecycleScope).also {
             lifecycleScope.launch { footerActionsController.init() }
@@ -110,7 +115,7 @@
             _panelFraction.value = value
         }
 
-    private val _squishinessFraction = MutableStateFlow(0f)
+    private val _squishinessFraction = MutableStateFlow(1f)
     var squishinessFractionValue: Float
         get() = _squishinessFraction.value
         set(value) {
@@ -131,7 +136,7 @@
     private val _headerAnimating = MutableStateFlow(false)
 
     private val _stackScrollerOverscrolling = MutableStateFlow(false)
-    var stackScrollerOverscrollingValue: Boolean
+    var isStackScrollerOverscrolling: Boolean
         get() = _stackScrollerOverscrolling.value
         set(value) {
             _stackScrollerOverscrolling.value = value
@@ -150,8 +155,6 @@
                 disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
             )
 
-    private val _showCollapsedOnKeyguard = MutableStateFlow(false)
-
     private val _keyguardAndExpanded = MutableStateFlow(false)
 
     /**
@@ -177,21 +180,65 @@
 
                 awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
             }
+            .onStart { emit(sysuiStatusBarStateController.state) }
             .stateIn(
                 lifecycleScope,
                 SharingStarted.WhileSubscribed(),
                 sysuiStatusBarStateController.state,
             )
 
+    private val isKeyguardState =
+        statusBarState
+            .map { it == StatusBarState.KEYGUARD }
+            .stateIn(
+                lifecycleScope,
+                SharingStarted.WhileSubscribed(),
+                statusBarState.value == StatusBarState.KEYGUARD,
+            )
+
     private val _viewHeight = MutableStateFlow(0)
 
     private val _headerTranslation = MutableStateFlow(0f)
 
     private val _inSplitShade = MutableStateFlow(false)
+    var isInSplitShade: Boolean
+        get() = _inSplitShade.value
+        set(value) {
+            _inSplitShade.value = value
+        }
 
     private val _transitioningToFullShade = MutableStateFlow(false)
+    var isTransitioningToFullShade: Boolean
+        get() = _transitioningToFullShade.value
+        set(value) {
+            _transitioningToFullShade.value = value
+        }
 
-    private val _lockscreenToShadeProgress = MutableStateFlow(false)
+    private val isBypassEnabled = deviceEntryInteractor.isBypassEnabled
+
+    private val showCollapsedOnKeyguard =
+        combine(
+                isBypassEnabled,
+                _transitioningToFullShade,
+                _inSplitShade,
+                ::calculateShowCollapsedOnKeyguard,
+            )
+            .stateIn(
+                lifecycleScope,
+                SharingStarted.WhileSubscribed(),
+                calculateShowCollapsedOnKeyguard(
+                    isBypassEnabled.value,
+                    isTransitioningToFullShade,
+                    isInSplitShade,
+                ),
+            )
+
+    private val _lockscreenToShadeProgress = MutableStateFlow(0.0f)
+    var lockscreenToShadeProgressValue: Float
+        get() = _lockscreenToShadeProgress.value
+        set(value) {
+            _lockscreenToShadeProgress.value = value
+        }
 
     private val _overscrolling = MutableStateFlow(false)
 
@@ -212,12 +259,32 @@
             _heightOverride.value = value
         }
 
+    private val forceQS =
+        combine(
+                _qsExpanded,
+                _stackScrollerOverscrolling,
+                isKeyguardState,
+                showCollapsedOnKeyguard,
+                ::calculateForceQs,
+            )
+            .stateIn(
+                lifecycleScope,
+                SharingStarted.WhileSubscribed(),
+                calculateForceQs(
+                    isQSExpanded,
+                    isStackScrollerOverscrolling,
+                    isKeyguardState.value,
+                    showCollapsedOnKeyguard.value,
+                ),
+            )
+
     val expansionState: StateFlow<QSExpansionState> =
-        combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> ->
-                val expansion = args[2] as Float
-                QSExpansionState(expansion.coerceIn(0f, 1f))
-            }
-            .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f))
+        combine(_qsExpansion, forceQS, ::calculateExpansionState)
+            .stateIn(
+                lifecycleScope,
+                SharingStarted.WhileSubscribed(),
+                calculateExpansionState(_qsExpansion.value, forceQS.value),
+            )
 
     /**
      * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
@@ -225,6 +292,16 @@
      */
     var collapseExpandAccessibilityAction: Runnable? = null
 
+    override suspend fun onActivated(): Nothing {
+        hydrateSquishinessInteractor()
+    }
+
+    private suspend fun hydrateSquishinessInteractor(): Nothing {
+        _squishinessFraction.collect {
+            squishinessInteractor.setSquishinessValue(it.constrainSquishiness())
+        }
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.asIndenting().run {
             printSection("Quick Settings state") {
@@ -238,13 +315,17 @@
                 println("panelExpansionFraction", panelExpansionFractionValue)
                 println("squishinessFraction", squishinessFractionValue)
                 println("expansionState", expansionState.value)
+                println("forceQS", forceQS.value)
             }
             printSection("Shade state") {
-                println("stackOverscrolling", stackScrollerOverscrollingValue)
+                println("stackOverscrolling", isStackScrollerOverscrolling)
                 println("statusBarState", StatusBarState.toString(statusBarState.value))
+                println("isKeyguardState", isKeyguardState.value)
                 println("isSmallScreen", isSmallScreenValue)
                 println("heightOverride", "${heightOverrideValue}px")
                 println("qqsHeaderHeight", "${qqsHeaderHeight.value}px")
+                println("isSplitShade", isInSplitShade)
+                println("showCollapsedOnKeyguard", showCollapsedOnKeyguard.value)
             }
         }
     }
@@ -257,3 +338,35 @@
     // In the future, this will have other relevant elements like squishiness.
     data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float)
 }
+
+private fun Float.constrainSquishiness(): Float {
+    return (0.1f + this * 0.9f).coerceIn(0f, 1f)
+}
+
+// Helper methods for combining flows.
+
+private fun calculateExpansionState(expansion: Float, forceQs: Boolean): QSExpansionState {
+    return if (forceQs) {
+        QSExpansionState(1f)
+    } else {
+        QSExpansionState(expansion.coerceIn(0f, 1f))
+    }
+}
+
+private fun calculateForceQs(
+    isQSExpanded: Boolean,
+    isStackOverScrolling: Boolean,
+    isKeyguardShowing: Boolean,
+    shouldShowCollapsedOnKeyguard: Boolean,
+): Boolean {
+    return (isQSExpanded || isStackOverScrolling) &&
+        (isKeyguardShowing && !shouldShowCollapsedOnKeyguard)
+}
+
+private fun calculateShowCollapsedOnKeyguard(
+    isBypassEnabled: Boolean,
+    isTransitioningToFullShade: Boolean,
+    isInSplitShade: Boolean,
+): Boolean {
+    return isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt
new file mode 100644
index 0000000..76ba9af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@SysUISingleton
+class TileSquishinessRepository @Inject constructor() {
+    private val _squishiness = MutableStateFlow(1f)
+    val squishiness = _squishiness.asStateFlow()
+
+    fun setSquishinessValue(value: Float) {
+        _squishiness.value = value
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt
new file mode 100644
index 0000000..4fdbc76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.data.repository.TileSquishinessRepository
+import javax.inject.Inject
+
+@SysUISingleton
+class TileSquishinessInteractor
+@Inject
+constructor(private val repository: TileSquishinessRepository) {
+    val squishiness = repository.squishiness
+
+    fun setSquishinessValue(value: Float) {
+        repository.setSquishinessValue(value)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index 8998a7f..a645b51 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -41,6 +41,7 @@
     val sizedTiles by
         viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList())
     val tiles = sizedTiles.fastMap { it.tile }
+    val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
 
     DisposableEffect(tiles) {
         val token = Any()
@@ -62,6 +63,7 @@
                 tile = it.tile,
                 iconOnly = it.isIcon,
                 modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+                squishiness = { squishiness },
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 8c2fb25..bf4c113 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -73,6 +73,7 @@
     secondaryLabel: String?,
     icon: Icon,
     colors: TileColors,
+    squishiness: () -> Float,
     accessibilityUiState: AccessibilityUiState? = null,
     toggleClickSupported: Boolean = false,
     iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
@@ -89,6 +90,7 @@
             modifier =
                 Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
                     Modifier.clip(iconShape)
+                        .verticalSquish(squishiness)
                         .background(colors.iconBackground, { 1f })
                         .combinedClickable(
                             onClick = onClick,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index e6edba5..3ba49ad 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileSquishinessViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
@@ -45,6 +46,7 @@
 constructor(
     private val iconTilesViewModel: IconTilesViewModel,
     private val gridSizeViewModel: FixedColumnsSizeViewModel,
+    private val squishinessViewModel: TileSquishinessViewModel,
 ) : PaginatableGridLayout {
 
     @Composable
@@ -60,6 +62,7 @@
         }
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
         val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
+        val squishiness by squishinessViewModel.squishiness.collectAsStateWithLifecycle()
 
         VerticalSpannedGrid(
             columns = columns,
@@ -72,6 +75,7 @@
                 tile = it.tile,
                 iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
                 modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+                squishiness = { squishiness },
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
new file mode 100644
index 0000000..ada1ef4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
@@ -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.systemui.qs.panels.ui.compose.infinitegrid
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import kotlin.math.roundToInt
+
+/**
+ * Modifier to squish the vertical bounds of a composable (usually a QS tile).
+ *
+ * It will squish the vertical bounds of the inner composable node by the value returned by
+ * [squishiness] on the measure/layout pass.
+ *
+ * The squished composable will be center aligned.
+ */
+fun Modifier.verticalSquish(squishiness: () -> Float): Modifier {
+    return layout { measurable, constraints ->
+        val placeable = measurable.measure(constraints)
+        val actualHeight = placeable.height
+        val squishedHeight = actualHeight * squishiness()
+        // Center the content by moving it UP (squishedHeight < actualHeight)
+        val scroll = (squishedHeight - actualHeight) / 2
+
+        layout(placeable.width, squishedHeight.roundToInt()) {
+            placeable.place(0, scroll.roundToInt())
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index afcbed6d..4bd5b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -98,7 +98,12 @@
 }
 
 @Composable
-fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) {
+fun Tile(
+    tile: TileViewModel,
+    iconOnly: Boolean,
+    squishiness: () -> Float,
+    modifier: Modifier = Modifier,
+) {
     val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
     val resources = resources()
     val uiState = remember(state, resources) { state.toUiState(resources) }
@@ -119,6 +124,7 @@
         onClick = tile::onClick,
         onLongClick = tile::onLongClick,
         uiState = uiState,
+        squishiness = squishiness,
         modifier = modifier,
     ) { expandable ->
         val icon = getTileIcon(icon = uiState.icon)
@@ -144,6 +150,7 @@
                 },
                 onLongClick = { tile.onLongClick(expandable) },
                 accessibilityUiState = uiState.accessibilityUiState,
+                squishiness = squishiness,
             )
         }
     }
@@ -155,12 +162,17 @@
     shape: Shape,
     iconOnly: Boolean,
     uiState: TileUiState,
+    squishiness: () -> Float,
     modifier: Modifier = Modifier,
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
     content: @Composable BoxScope.(Expandable) -> Unit,
 ) {
-    Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) {
+    Expandable(
+        color = color,
+        shape = shape,
+        modifier = modifier.clip(shape).verticalSquish(squishiness),
+    ) {
         val longPressLabel = longPressLabel()
         Box(
             modifier =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index eee905f..88e3019 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -42,6 +42,7 @@
     tilesInteractor: CurrentTilesInteractor,
     fixedColumnsSizeViewModel: FixedColumnsSizeViewModel,
     quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor,
+    val squishinessViewModel: TileSquishinessViewModel,
     private val iconTilesViewModel: IconTilesViewModel,
     @Application private val applicationScope: CoroutineScope,
 ) {
@@ -52,7 +53,7 @@
         quickQuickSettingsRowInteractor.rows.stateIn(
             applicationScope,
             SharingStarted.WhileSubscribed(),
-            quickQuickSettingsRowInteractor.defaultRows
+            quickQuickSettingsRowInteractor.defaultRows,
         )
 
     val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> =
@@ -60,12 +61,7 @@
             .flatMapLatest { columns ->
                 tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) ->
                     tiles
-                        .map {
-                            SizedTileImpl(
-                                TileViewModel(it.tile, it.spec),
-                                it.spec.width,
-                            )
-                        }
+                        .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
                         .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
                 }
             }
@@ -73,15 +69,10 @@
                 applicationScope,
                 SharingStarted.WhileSubscribed(),
                 tilesInteractor.currentTiles.value
-                    .map {
-                        SizedTileImpl(
-                            TileViewModel(it.tile, it.spec),
-                            it.spec.width,
-                        )
-                    }
+                    .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
                     .let {
                         splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten()
-                    }
+                    },
             )
 
     private val TileSpec.width: Int
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt
new file mode 100644
index 0000000..0c4d5de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.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.qs.panels.ui.viewmodel
+
+import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
+import javax.inject.Inject
+
+/** View model to track the squishiness of tiles. */
+class TileSquishinessViewModel
+@Inject
+constructor(tileSquishinessInteractor: TileSquishinessInteractor) {
+    val squishiness = tileSquishinessInteractor.squishiness
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index a7e7d8b..a8be580 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -108,7 +108,11 @@
                 traceName = "SceneWindowRootViewBinder",
                 minWindowLifecycleState = WindowLifecycleState.ATTACHED,
                 factory = {
-                    viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+                    viewModelFactory.create(
+                        view,
+                        view.context.displayId,
+                        motionEventHandlerReceiver,
+                    )
                 },
             ) { viewModel ->
                 try {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
index 5ff507a..fc172e8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
@@ -16,16 +16,13 @@
 
 package com.android.systemui.scene.ui.viewmodel
 
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.dualShadeActions
+import com.android.systemui.shade.ui.viewmodel.singleShadeActions
+import com.android.systemui.shade.ui.viewmodel.splitShadeActions
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 
@@ -36,41 +33,21 @@
     override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
         shadeInteractor.shadeMode.collect { shadeMode ->
             setActions(
-                when (shadeMode) {
-                    ShadeMode.Single -> singleShadeActions()
-                    ShadeMode.Split -> splitShadeActions()
-                    ShadeMode.Dual -> dualShadeActions()
-                }
+                buildList {
+                        addAll(
+                            when (shadeMode) {
+                                ShadeMode.Single ->
+                                    singleShadeActions(requireTwoPointersForTopEdgeForQs = true)
+                                ShadeMode.Split -> splitShadeActions()
+                                ShadeMode.Dual -> dualShadeActions()
+                            }
+                        )
+                    }
+                    .associate { it }
             )
         }
     }
 
-    private fun singleShadeActions(): Map<UserAction, UserActionResult> {
-        return mapOf(
-            Swipe.Down to Scenes.Shade,
-            swipeDownFromTopWithTwoFingers() to Scenes.QuickSettings,
-        )
-    }
-
-    private fun splitShadeActions(): Map<UserAction, UserActionResult> {
-        return mapOf(
-            Swipe.Down to UserActionResult(Scenes.Shade, ToSplitShade),
-            swipeDownFromTopWithTwoFingers() to UserActionResult(Scenes.Shade, ToSplitShade),
-        )
-    }
-
-    private fun dualShadeActions(): Map<UserAction, UserActionResult> {
-        return mapOf(
-            Swipe.Down to Overlays.NotificationsShade,
-            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
-                Overlays.QuickSettingsShade,
-        )
-    }
-
-    private fun swipeDownFromTopWithTwoFingers(): UserAction {
-        return Swipe(direction = SwipeDirection.Down, pointerCount = 2, fromSource = Edge.Top)
-    }
-
     @AssistedFactory
     interface Factory {
         fun create(): GoneUserActionsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
new file mode 100644
index 0000000..4ef8e0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * Models haptics UI state for the scene container.
+ *
+ * This model gets a [View] to play haptics using the [View.performHapticFeedback] API. This should
+ * be the only purpose of this reference.
+ */
+class SceneContainerHapticsViewModel
+@AssistedInject
+constructor(
+    @Assisted private val view: View,
+    sceneInteractor: SceneInteractor,
+    shadeInteractor: ShadeInteractor,
+    private val msdlPlayer: MSDLPlayer,
+) : ExclusiveActivatable() {
+
+    /** Should haptics be played by pulling down the shade */
+    private val isShadePullHapticsRequired: Flow<Boolean> =
+        combine(shadeInteractor.isUserInteracting, sceneInteractor.transitionState) {
+                interacting,
+                transitionState ->
+                interacting && transitionState.isValidForShadePullHaptics()
+            }
+            .distinctUntilChanged()
+
+    override suspend fun onActivated(): Nothing {
+        isShadePullHapticsRequired.collect { playShadePullHaptics ->
+            if (!playShadePullHaptics) return@collect
+
+            if (Flags.msdlFeedback()) {
+                msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+            } else {
+                view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
+            }
+        }
+        awaitCancellation()
+    }
+
+    private fun ObservableTransitionState.isValidForShadePullHaptics(): Boolean {
+        val validOrigin =
+            isTransitioning(from = Scenes.Gone) || isTransitioning(from = Scenes.Lockscreen)
+        val validDestination =
+            isTransitioning(to = Scenes.Shade) ||
+                isTransitioning(to = Scenes.QuickSettings) ||
+                isTransitioning(to = Overlays.QuickSettingsShade) ||
+                isTransitioning(to = Overlays.NotificationsShade)
+        return validOrigin && validDestination
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(view: View): SceneContainerHapticsViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 0bf2d49..f505385 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.scene.ui.viewmodel
 
 import android.view.MotionEvent
+import android.view.View
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.geometry.Offset
+import com.android.app.tracing.coroutines.launch
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.compose.animation.scene.ObservableTransitionState
@@ -60,6 +62,8 @@
     private val splitEdgeDetector: SplitEdgeDetector,
     private val logger: SceneLogger,
     gestureFilterFactory: SceneContainerGestureFilter.Factory,
+    hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
+    @Assisted view: View,
     @Assisted displayId: Int,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
 ) : ExclusiveActivatable() {
@@ -72,6 +76,8 @@
     /** Whether the container is visible. */
     val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
 
+    private val hapticsViewModel = hapticsViewModelFactory.create(view)
+
     /**
      * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
      * [UserAction]s for this container.
@@ -107,6 +113,7 @@
             coroutineScope {
                 launch { hydrator.activate() }
                 launch { gestureFilter.activate() }
+                launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() }
             }
             awaitCancellation()
         } finally {
@@ -281,6 +288,7 @@
     @AssistedFactory
     interface Factory {
         fun create(
+            view: View,
             displayId: Int,
             motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
         ): SceneContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 0c05dbd..5896659 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -23,6 +23,7 @@
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
+import static com.android.systemui.Flags.msdlFeedback;
 import static com.android.systemui.Flags.predictiveBackAnimateShade;
 import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
@@ -236,6 +237,9 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 
+import com.google.android.msdl.data.model.MSDLToken;
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import kotlin.Unit;
 
 import kotlinx.coroutines.CoroutineDispatcher;
@@ -312,6 +316,7 @@
     private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener();
     private final NotificationPanelView mView;
     private final VibratorHelper mVibratorHelper;
+    private final MSDLPlayer mMSDLPlayer;
     private final MetricsLogger mMetricsLogger;
     private final ConfigurationController mConfigurationController;
     private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder;
@@ -777,7 +782,8 @@
             SplitShadeStateController splitShadeStateController,
             PowerInteractor powerInteractor,
             KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
-            NaturalScrollingSettingObserver naturalScrollingSettingObserver) {
+            NaturalScrollingSettingObserver naturalScrollingSettingObserver,
+            MSDLPlayer msdlPlayer) {
         SceneContainerFlag.assertInLegacyMode();
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
@@ -855,6 +861,7 @@
         mNotificationsDragEnabled = mResources.getBoolean(
                 R.bool.config_enableNotificationShadeDrag);
         mVibratorHelper = vibratorHelper;
+        mMSDLPlayer = msdlPlayer;
         mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
         mSystemClock = systemClock;
@@ -2911,7 +2918,7 @@
         }
 
         if (!mStatusBarStateController.isDozing()) {
-            mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
+            performHapticFeedback(HapticFeedbackConstants.REJECT);
         }
     }
 
@@ -3279,7 +3286,20 @@
     }
 
     public void performHapticFeedback(int constant) {
-        mVibratorHelper.performHapticFeedback(mView, constant);
+        if (msdlFeedback()) {
+            MSDLToken token;
+            switch (constant) {
+                case HapticFeedbackConstants.GESTURE_START ->
+                        token = MSDLToken.SWIPE_THRESHOLD_INDICATOR;
+                case HapticFeedbackConstants.REJECT -> token = MSDLToken.FAILURE;
+                default -> token = null;
+            }
+            if (token != null) {
+                mMSDLPlayer.playToken(token, null);
+            }
+        } else {
+            mVibratorHelper.performHapticFeedback(mView, constant);
+        }
     }
 
     private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker {
@@ -3736,10 +3756,7 @@
     private void maybeVibrateOnOpening(boolean openingWithTouch) {
         if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
             if (!openingWithTouch || !mHasVibratedOnOpen) {
-                mVibratorHelper.performHapticFeedback(
-                        mView,
-                        HapticFeedbackConstants.GESTURE_START
-                );
+                performHapticFeedback(HapticFeedbackConstants.GESTURE_START);
                 mHasVibratedOnOpen = true;
                 mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
new file mode 100644
index 0000000..65b6231
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.shade.ui.viewmodel
+
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */
+fun singleShadeActions(
+    requireTwoPointersForTopEdgeForQs: Boolean = false
+): Array<Pair<UserAction, UserActionResult>> {
+    return arrayOf(
+        // Swiping down, not from the edge, always goes to shade.
+        Swipe.Down to Scenes.Shade,
+        swipeDown(pointerCount = 2) to Scenes.Shade,
+
+        // Swiping down from the top edge.
+        swipeDownFromTop(pointerCount = 1) to
+            if (requireTwoPointersForTopEdgeForQs) {
+                Scenes.Shade
+            } else {
+                Scenes.QuickSettings
+            },
+        swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings,
+    )
+}
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the split shade. */
+fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
+    val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade)
+    return arrayOf(
+        // Swiping down, not from the edge, always goes to shade.
+        Swipe.Down to splitShadeSceneKey,
+        swipeDown(pointerCount = 2) to splitShadeSceneKey,
+        // Swiping down from the top edge goes to QS.
+        swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey,
+        swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey,
+    )
+}
+
+/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */
+fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
+    return arrayOf(
+        Swipe.Down to Overlays.NotificationsShade,
+        Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+            Overlays.QuickSettingsShade,
+    )
+}
+
+private fun swipeDownFromTop(pointerCount: Int): Swipe {
+    return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
+}
+
+private fun swipeDown(pointerCount: Int): Swipe {
+    return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
index f8a850a..cc6e8c2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
@@ -16,11 +16,13 @@
 
 package com.android.systemui.shade.ui.viewmodel
 
+import com.android.app.tracing.coroutines.flow.map
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.scene.domain.interactor.SceneBackInteractor
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
@@ -41,21 +43,23 @@
 constructor(
     private val qsSceneAdapter: QSSceneAdapter,
     private val shadeInteractor: ShadeInteractor,
+    private val sceneBackInteractor: SceneBackInteractor,
 ) : UserActionsViewModel() {
 
     override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
         combine(
                 shadeInteractor.shadeMode,
                 qsSceneAdapter.isCustomizerShowing,
-            ) { shadeMode, isCustomizerShowing ->
+                sceneBackInteractor.backScene.map { it ?: SceneFamilies.Home },
+            ) { shadeMode, isCustomizerShowing, backScene ->
                 buildMap<UserAction, UserActionResult> {
                     if (!isCustomizerShowing) {
                         set(
                             Swipe(SwipeDirection.Up),
                             UserActionResult(
-                                SceneFamilies.Home,
-                                ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
-                            )
+                                backScene,
+                                ToSplitShade.takeIf { shadeMode is ShadeMode.Split },
+                            ),
                         )
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt
new file mode 100644
index 0000000..57c8bc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.core
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.os.Binder
+import android.os.RemoteException
+import android.view.WindowInsets
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.RegisterStatusBarResult
+import com.android.systemui.CoreStartable
+import com.android.systemui.InitController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.navigationbar.NavigationBarController
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import dagger.Lazy
+import javax.inject.Inject
+
+@SysUISingleton
+class CommandQueueInitializer
+@Inject
+constructor(
+    private val context: Context,
+    private val commandQueue: CommandQueue,
+    private val commandQueueCallbacksLazy: Lazy<CommandQueue.Callbacks>,
+    private val statusBarModeRepository: StatusBarModeRepositoryStore,
+    private val initController: InitController,
+    private val barService: IStatusBarService,
+    private val navigationBarController: NavigationBarController,
+) : CoreStartable {
+
+    override fun start() {
+        StatusBarSimpleFragment.assertInNewMode()
+        val result: RegisterStatusBarResult =
+            try {
+                barService.registerStatusBar(commandQueue)
+            } catch (ex: RemoteException) {
+                ex.rethrowFromSystemServer()
+                return
+            }
+
+        createNavigationBar(result)
+
+        if ((result.mTransientBarTypes and WindowInsets.Type.statusBars()) != 0) {
+            statusBarModeRepository.defaultDisplay.showTransient()
+        }
+        val displayId = context.display.displayId
+        val commandQueueCallbacks = commandQueueCallbacksLazy.get()
+        commandQueueCallbacks.onSystemBarAttributesChanged(
+            displayId,
+            result.mAppearance,
+            result.mAppearanceRegions,
+            result.mNavbarColorManagedByIme,
+            result.mBehavior,
+            result.mRequestedVisibleTypes,
+            result.mPackageName,
+            result.mLetterboxDetails,
+        )
+
+        // StatusBarManagerService has a back up of IME token and it's restored here.
+        commandQueueCallbacks.setImeWindowStatus(
+            displayId,
+            result.mImeWindowVis,
+            result.mImeBackDisposition,
+            result.mShowImeSwitcher,
+        )
+
+        // Set up the initial icon state
+        val numIcons: Int = result.mIcons.size
+        for (i in 0 until numIcons) {
+            commandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i))
+        }
+
+        // set the initial view visibility
+        val disabledFlags1 = result.mDisabledFlags1
+        val disabledFlags2 = result.mDisabledFlags2
+        initController.addPostInitTask {
+            commandQueue.disable(displayId, disabledFlags1, disabledFlags2, /* animate= */ false)
+            try {
+                // NOTE(b/262059863): Force-update the disable flags after applying the flags
+                // returned from registerStatusBar(). The result's disabled flags may be stale
+                // if StatusBarManager's disabled flags are updated between registering the bar
+                // and this handling this post-init task. We force an update in this case, and use a
+                // new token to not conflict with any other disabled flags already requested by
+                // SysUI
+                val token = Binder()
+                barService.disable(StatusBarManager.DISABLE_HOME, token, context.packageName)
+                barService.disable(0, token, context.packageName)
+            } catch (ex: RemoteException) {
+                ex.rethrowFromSystemServer()
+            }
+        }
+    }
+
+    private fun createNavigationBar(result: RegisterStatusBarResult) {
+        navigationBarController.createNavigationBars(/* includeDefaultDisplay= */ true, result)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
new file mode 100644
index 0000000..8bd990b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.core
+
+import android.view.View
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.PluginDependencyProvider
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.shade.NotificationShadeWindowViewController
+import com.android.systemui.shade.ShadeSurface
+import com.android.systemui.statusbar.AutoHideUiElement
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import com.android.systemui.statusbar.phone.AutoHideController
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore
+import com.android.wm.shell.bubbles.Bubbles
+import dagger.Lazy
+import java.io.PrintWriter
+import java.util.Optional
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/**
+ * Class responsible for managing the lifecycle and state of the status bar.
+ *
+ * It is a temporary class, created to pull status bar related logic out of CentralSurfacesImpl. The
+ * plan is break it out into individual classes.
+ */
+@SysUISingleton
+class StatusBarOrchestrator
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val statusBarInitializer: StatusBarInitializer,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val statusBarModeRepository: StatusBarModeRepositoryStore,
+    private val demoModeController: DemoModeController,
+    private val pluginDependencyProvider: PluginDependencyProvider,
+    private val autoHideController: AutoHideController,
+    private val remoteInputManager: NotificationRemoteInputManager,
+    private val notificationShadeWindowViewControllerLazy:
+    Lazy<NotificationShadeWindowViewController>,
+    private val shadeSurface: ShadeSurface,
+    private val bubblesOptional: Optional<Bubbles>,
+    private val statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore,
+    powerInteractor: PowerInteractor,
+    primaryBouncerInteractor: PrimaryBouncerInteractor,
+) : CoreStartable {
+
+    private val phoneStatusBarViewController =
+        MutableStateFlow<PhoneStatusBarViewController?>(value = null)
+
+    private val phoneStatusBarTransitions =
+        MutableStateFlow<PhoneStatusBarTransitions?>(value = null)
+
+    private val shouldAnimateNextBarModeChange =
+        combine(
+            statusBarModeRepository.defaultDisplay.isTransientShown,
+            powerInteractor.isAwake,
+            statusBarWindowStateRepositoryStore.defaultDisplay.windowState,
+        ) { isTransientShown, isDeviceAwake, statusBarWindowState ->
+            !isTransientShown &&
+                isDeviceAwake &&
+                statusBarWindowState != StatusBarWindowState.Hidden
+        }
+
+    private val controllerAndBouncerShowing =
+        combine(
+            phoneStatusBarViewController.filterNotNull(),
+            primaryBouncerInteractor.isShowing,
+            ::Pair,
+        )
+
+    private val barTransitionsAndDeviceAsleep =
+        combine(phoneStatusBarTransitions.filterNotNull(), powerInteractor.isAsleep, ::Pair)
+
+    private val statusBarVisible =
+        combine(
+            statusBarModeRepository.defaultDisplay.statusBarMode,
+            statusBarWindowStateRepositoryStore.defaultDisplay.windowState,
+        ) { mode, statusBarWindowState ->
+            mode != StatusBarMode.LIGHTS_OUT &&
+                mode != StatusBarMode.LIGHTS_OUT_TRANSPARENT &&
+                statusBarWindowState != StatusBarWindowState.Hidden
+        }
+
+    private val barModeUpdate =
+        combine(
+                shouldAnimateNextBarModeChange,
+                phoneStatusBarTransitions.filterNotNull(),
+                statusBarModeRepository.defaultDisplay.statusBarMode,
+                ::Triple,
+            )
+            .distinctUntilChangedBy { (_, barTransitions, statusBarMode) ->
+                // We only want to collect when either bar transitions or status bar mode
+                // changed.
+                Pair(barTransitions, statusBarMode)
+            }
+
+    override fun start() {
+        StatusBarSimpleFragment.assertInNewMode()
+        applicationScope.launch {
+            launch {
+                controllerAndBouncerShowing.collect { (controller, bouncerShowing) ->
+                    setBouncerShowingForStatusBarComponents(controller, bouncerShowing)
+                }
+            }
+            launch {
+                barTransitionsAndDeviceAsleep.collect { (barTransitions, deviceAsleep) ->
+                    if (deviceAsleep) {
+                        barTransitions.finishAnimations()
+                    }
+                }
+            }
+            launch { statusBarVisible.collect { updateBubblesVisibility(it) } }
+            launch {
+                barModeUpdate.collect { (animate, barTransitions, statusBarMode) ->
+                    updateBarMode(animate, barTransitions, statusBarMode)
+                }
+            }
+        }
+        createAndAddWindow()
+        setupPluginDependencies()
+        setUpAutoHide()
+    }
+
+    private fun createAndAddWindow() {
+        initializeStatusBarFragment()
+        statusBarWindowController.attach()
+    }
+
+    private fun initializeStatusBarFragment() {
+        statusBarInitializer.statusBarViewUpdatedListener =
+            object : StatusBarInitializer.OnStatusBarViewUpdatedListener {
+                override fun onStatusBarViewUpdated(
+                    statusBarViewController: PhoneStatusBarViewController,
+                    statusBarTransitions: PhoneStatusBarTransitions,
+                ) {
+                    phoneStatusBarViewController.value = statusBarViewController
+                    phoneStatusBarTransitions.value = statusBarTransitions
+
+                    notificationShadeWindowViewControllerLazy
+                        .get()
+                        .setStatusBarViewController(statusBarViewController)
+                    // Ensure we re-propagate panel expansion values to the panel controller and
+                    // any listeners it may have, such as PanelBar. This will also ensure we
+                    // re-display the notification panel if necessary (for example, if
+                    // a heads-up notification was being displayed and should continue being
+                    // displayed).
+                    shadeSurface.updateExpansionAndVisibility()
+                }
+            }
+    }
+
+    private fun setupPluginDependencies() {
+        pluginDependencyProvider.allowPluginDependency(DarkIconDispatcher::class.java)
+        pluginDependencyProvider.allowPluginDependency(StatusBarStateController::class.java)
+    }
+
+    private fun setUpAutoHide() {
+        autoHideController.setStatusBar(
+            object : AutoHideUiElement {
+                override fun synchronizeState() {}
+
+                override fun shouldHideOnTouch(): Boolean {
+                    return !remoteInputManager.isRemoteInputActive
+                }
+
+                override fun isVisible(): Boolean {
+                    return statusBarModeRepository.defaultDisplay.isTransientShown.value
+                }
+
+                override fun hide() {
+                    statusBarModeRepository.defaultDisplay.clearTransient()
+                }
+            })
+    }
+
+    private fun updateBarMode(
+        animate: Boolean,
+        barTransitions: PhoneStatusBarTransitions,
+        barMode: StatusBarMode,
+    ) {
+        if (!demoModeController.isInDemoMode) {
+            barTransitions.transitionTo(barMode.toTransitionModeInt(), animate)
+        }
+        autoHideController.touchAutoHide()
+    }
+
+    private fun updateBubblesVisibility(statusBarVisible: Boolean) {
+        bubblesOptional.ifPresent { bubbles: Bubbles ->
+            bubbles.onStatusBarVisibilityChanged(statusBarVisible)
+        }
+    }
+
+    private fun setBouncerShowingForStatusBarComponents(
+        controller: PhoneStatusBarViewController,
+        bouncerShowing: Boolean,
+    ) {
+        val importance =
+            if (bouncerShowing) {
+                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+            } else {
+                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+            }
+        controller.setImportantForAccessibility(importance)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println(statusBarWindowStateRepositoryStore.defaultDisplay.windowState.value)
+        CentralSurfaces.dumpBarTransitions(
+            pw,
+            "PhoneStatusBarTransitions",
+            phoneStatusBarTransitions.value,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 3903ff3..cf238d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -46,6 +46,7 @@
  */
 @Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class])
 abstract class StatusBarModule {
+
     @Binds
     @IntoMap
     @ClassKey(OngoingCallController::class)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index da29b0f..ec5ebc36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -43,7 +43,7 @@
         entry: NotificationEntry,
         builder: Notification.Builder,
         systemUIContext: Context,
-        packageContext: Context
+        packageContext: Context,
     ): RichOngoingContentModel?
 }
 
@@ -52,7 +52,7 @@
         entry: NotificationEntry,
         builder: Notification.Builder,
         systemUIContext: Context,
-        packageContext: Context
+        packageContext: Context,
     ): RichOngoingContentModel? = null
 }
 
@@ -68,7 +68,7 @@
         entry: NotificationEntry,
         builder: Notification.Builder,
         systemUIContext: Context,
-        packageContext: Context
+        packageContext: Context,
     ): RichOngoingContentModel? {
         val sbn = entry.sbn
         val notification = sbn.notification
@@ -89,7 +89,7 @@
                         null
                     }
                 }
-            } else if (builder.style is Notification.EnRouteStyle) {
+            } else if (builder.style is Notification.ProgressStyle) {
                 parseEnRouteNotification(notification, icon)
             } else null
         } catch (e: Exception) {
@@ -104,7 +104,7 @@
      */
     private fun parseTimerNotification(
         notification: Notification,
-        icon: IconModel
+        icon: IconModel,
     ): TimerContentModel {
         // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57
         // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06
@@ -132,7 +132,7 @@
                             resumeIntent = notification.findStartIntent(),
                             addMinuteAction = notification.findAddMinuteAction(),
                             resetAction = notification.findResetAction(),
-                        )
+                        ),
                 )
             }
             "RUNNING" -> {
@@ -149,7 +149,7 @@
                             pauseIntent = notification.findPauseIntent(),
                             addMinuteAction = notification.findAddMinuteAction(),
                             resetAction = notification.findResetAction(),
-                        )
+                        ),
                 )
             }
             else -> error("unknown state ($state) in sortKey=$sortKey")
@@ -192,7 +192,7 @@
         val localDateTime =
             LocalDateTime.of(
                 LocalDate.now(),
-                LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000)
+                LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000),
             )
         val offset = ZoneId.systemDefault().rules.getOffset(localDateTime)
         return localDateTime.toInstant(offset).toEpochMilli()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 57be629..0ad22e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -132,6 +133,7 @@
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
         PrimaryBouncerToLockscreenTransitionViewModel,
@@ -444,6 +446,7 @@
             occludedToAodTransitionViewModel.lockscreenAlpha,
             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
             occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+            offToLockscreenTransitionViewModel.lockscreenAlpha,
             primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
             glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
             lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1d3f0e1..5f4f72f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -241,10 +241,10 @@
 import com.android.wm.shell.startingsurface.SplashscreenContentDrawer;
 import com.android.wm.shell.startingsurface.StartingSurface;
 
-import dagger.Lazy;
-
 import dalvik.annotation.optimization.NeverCompile;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Map;
@@ -304,6 +304,7 @@
             };
 
     void onStatusBarWindowStateChanged(@WindowVisibleState int state) {
+        StatusBarSimpleFragment.assertInLegacyMode();
         mStatusBarWindowState = state;
         updateBubblesVisibility();
     }
@@ -813,8 +814,9 @@
         mStartingSurfaceOptional = startingSurfaceOptional;
         mDreamManager = dreamManager;
         lockscreenShadeTransitionController.setCentralSurfaces(this);
-        statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
-
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
+        }
         mScreenOffAnimationController = screenOffAnimationController;
 
         ShadeExpansionListener shadeExpansionListener = this::onPanelExpansionChanged;
@@ -901,10 +903,12 @@
         mWallpaperSupported = mWallpaperManager.isWallpaperSupported();
 
         RegisterStatusBarResult result = null;
-        try {
-            result = mBarService.registerStatusBar(mCommandQueue);
-        } catch (RemoteException ex) {
-            ex.rethrowFromSystemServer();
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            try {
+                result = mBarService.registerStatusBar(mCommandQueue);
+            } catch (RemoteException ex) {
+                ex.rethrowFromSystemServer();
+            }
         }
 
         createAndAddWindows(result);
@@ -912,30 +916,45 @@
         // Set up the initial notification state. This needs to happen before CommandQueue.disable()
         setUpPresenter();
 
-        if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
-            mStatusBarModeRepository.getDefaultDisplay().showTransient();
-        }
-        mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
-                result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior,
-                result.mRequestedVisibleTypes, result.mPackageName, result.mLetterboxDetails);
-
-        // StatusBarManagerService has a back up of IME token and it's restored here.
-        mCommandQueueCallbacks.setImeWindowStatus(mDisplayId, result.mImeWindowVis,
-                result.mImeBackDisposition, result.mShowImeSwitcher);
-
-        // Set up the initial icon state
-        int numIcons = result.mIcons.size();
-        for (int i = 0; i < numIcons; i++) {
-            mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i));
-        }
-
-        if (DEBUG) {
-            Log.d(TAG, String.format(
-                    "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x",
-                    numIcons,
-                    result.mDisabledFlags1,
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
+                mStatusBarModeRepository.getDefaultDisplay().showTransient();
+            }
+            mCommandQueueCallbacks.onSystemBarAttributesChanged(
+                    mDisplayId,
                     result.mAppearance,
-                    result.mImeWindowVis));
+                    result.mAppearanceRegions,
+                    result.mNavbarColorManagedByIme,
+                    result.mBehavior,
+                    result.mRequestedVisibleTypes,
+                    result.mPackageName,
+                    result.mLetterboxDetails);
+
+            // StatusBarManagerService has a back up of IME token and it's restored here.
+            mCommandQueueCallbacks.setImeWindowStatus(
+                    mDisplayId,
+                    result.mImeWindowVis,
+                    result.mImeBackDisposition,
+                    result.mShowImeSwitcher);
+
+            // Set up the initial icon state
+            int numIcons = result.mIcons.size();
+            for (int i = 0; i < numIcons; i++) {
+                mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i));
+            }
+
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        String.format(
+                                "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x",
+                                numIcons,
+                                result.mDisabledFlags1,
+                                result.mAppearance,
+                                result.mImeWindowVis));
+            }
         }
 
         IntentFilter internalFilter = new IntentFilter();
@@ -1005,24 +1024,30 @@
 
         mAccessibilityFloatingMenuController.init();
 
-        // set the initial view visibility
-        int disabledFlags1 = result.mDisabledFlags1;
-        int disabledFlags2 = result.mDisabledFlags2;
-        mInitController.addPostInitTask(() -> {
-            setUpDisableFlags(disabledFlags1, disabledFlags2);
-            try {
-                // NOTE(b/262059863): Force-update the disable flags after applying the flags
-                // returned from registerStatusBar(). The result's disabled flags may be stale
-                // if StatusBarManager's disabled flags are updated between registering the bar and
-                // this handling this post-init task. We force an update in this case, and use a new
-                // token to not conflict with any other disabled flags already requested by SysUI
-                Binder token = new Binder();
-                mBarService.disable(DISABLE_HOME, token, mContext.getPackageName());
-                mBarService.disable(0, token, mContext.getPackageName());
-            } catch (RemoteException ex) {
-                ex.rethrowFromSystemServer();
-            }
-        });
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            // set the initial view visibility
+            int disabledFlags1 = result.mDisabledFlags1;
+            int disabledFlags2 = result.mDisabledFlags2;
+            mInitController.addPostInitTask(
+                    () -> {
+                        setUpDisableFlags(disabledFlags1, disabledFlags2);
+                        try {
+                            // NOTE(b/262059863): Force-update the disable flags after applying the
+                            // flags returned from registerStatusBar(). The result's disabled flags
+                            // may be stale if StatusBarManager's disabled flags are updated between
+                            // registering the bar and this handling this post-init task. We force
+                            // an update in this case, and use a new token to not conflict with any
+                            // other disabled flags already requested by SysUI
+                            Binder token = new Binder();
+                            mBarService.disable(DISABLE_HOME, token, mContext.getPackageName());
+                            mBarService.disable(0, token, mContext.getPackageName());
+                        } catch (RemoteException ex) {
+                            ex.rethrowFromSystemServer();
+                        }
+                    });
+        }
 
         registerCallbacks();
 
@@ -1101,7 +1126,7 @@
 
     /**
      * @deprecated use {@link
-     * WindowRootViewVisibilityInteractor.isLockscreenOrShadeVisible} instead.
+     * WindowRootViewVisibilityInteractor#isLockscreenOrShadeVisible()} instead.
      */    @VisibleForTesting
     @Deprecated
     void initShadeVisibilityListener() {
@@ -1168,13 +1193,16 @@
         mWallpaperController.setRootView(getNotificationShadeWindowView());
 
         mDemoModeController.addCallback(mDemoModeCallback);
-        mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.getDefaultDisplay().isTransientShown(),
-                this::onTransientShownChanged);
-        mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(),
-                this::updateBarMode);
-
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator.
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            mJavaAdapter.alwaysCollectFlow(
+                    mStatusBarModeRepository.getDefaultDisplay().isTransientShown(),
+                    this::onTransientShownChanged);
+            mJavaAdapter.alwaysCollectFlow(
+                    mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(),
+                    this::updateBarMode);
+        }
         mCommandQueueCallbacks = mCommandQueueCallbacksLazy.get();
         mCommandQueue.addCallback(mCommandQueueCallbacks);
 
@@ -1184,59 +1212,70 @@
                 mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
         mWakeUpCoordinator.onPanelExpansionChanged(currentState);
 
-        // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
-        mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
-        mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
-
-        // Set up CollapsedStatusBarFragment and PhoneStatusBarView
-        mStatusBarInitializer.setStatusBarViewUpdatedListener(
-                (statusBarViewController, statusBarTransitions) -> {
-                    mPhoneStatusBarViewController = statusBarViewController;
-                    mStatusBarTransitions = statusBarTransitions;
-                    getNotificationShadeWindowViewController()
-                            .setStatusBarViewController(mPhoneStatusBarViewController);
-                    // Ensure we re-propagate panel expansion values to the panel controller and
-                    // any listeners it may have, such as PanelBar. This will also ensure we
-                    // re-display the notification panel if necessary (for example, if
-                    // a heads-up notification was being displayed and should continue being
-                    // displayed).
-                    mShadeSurface.updateExpansionAndVisibility();
-                    setBouncerShowingForStatusBarComponents(mBouncerShowing);
-                    checkBarModes();
-                });
-        // When the flag is on, we register the fragment as a core startable and this is not needed
+        // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in
+        // StatusBarOrchestrator.
         if (!StatusBarSimpleFragment.isEnabled()) {
+            // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
+            mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
+            mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
+
+            // Set up CollapsedStatusBarFragment and PhoneStatusBarView
+            mStatusBarInitializer.setStatusBarViewUpdatedListener(
+                    (statusBarViewController, statusBarTransitions) -> {
+
+                        mPhoneStatusBarViewController = statusBarViewController;
+                        mStatusBarTransitions = statusBarTransitions;
+                        getNotificationShadeWindowViewController()
+                                .setStatusBarViewController(mPhoneStatusBarViewController);
+                        // Ensure we re-propagate panel expansion values to the panel controller and
+                        // any listeners it may have, such as PanelBar. This will also ensure we
+                        // re-display the notification panel if necessary (for example, if
+                        // a heads-up notification was being displayed and should continue being
+                        // displayed).
+                        mShadeSurface.updateExpansionAndVisibility();
+                        setBouncerShowingForStatusBarComponents(mBouncerShowing);
+                        checkBarModes();
+                    });
+            // When the flag is on, we register the fragment as a core startable and this is not
+            // needed
             mStatusBarInitializer.initializeStatusBar();
         }
 
         mStatusBarTouchableRegionManager.setup(getNotificationShadeWindowView());
 
-        createNavigationBar(result);
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            createNavigationBar(result);
+        }
 
         mAmbientIndicationContainer = getNotificationShadeWindowView().findViewById(
                 R.id.ambient_indication_container);
 
-        mAutoHideController.setStatusBar(new AutoHideUiElement() {
-            @Override
-            public void synchronizeState() {
-                checkBarModes();
-            }
+        // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in
+        // StatusBarOrchestrator.
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            mAutoHideController.setStatusBar(
+                    new AutoHideUiElement() {
+                        @Override
+                        public void synchronizeState() {
+                            checkBarModes();
+                        }
 
-            @Override
-            public boolean shouldHideOnTouch() {
-                return !mRemoteInputManager.isRemoteInputActive();
-            }
+                        @Override
+                        public boolean shouldHideOnTouch() {
+                            return !mRemoteInputManager.isRemoteInputActive();
+                        }
 
-            @Override
-            public boolean isVisible() {
-                return isTransientShown();
-            }
+                        @Override
+                        public boolean isVisible() {
+                            return isTransientShown();
+                        }
 
-            @Override
-            public void hide() {
-                mStatusBarModeRepository.getDefaultDisplay().clearTransient();
-            }
-        });
+                        @Override
+                        public void hide() {
+                            mStatusBarModeRepository.getDefaultDisplay().clearTransient();
+                        }
+                    });
+        }
 
         ScrimView scrimBehind = getNotificationShadeWindowView().findViewById(R.id.scrim_behind);
         ScrimView notificationsScrim = getNotificationShadeWindowView()
@@ -1479,12 +1518,14 @@
      * @param state2 disable2 flags
      */
     protected void setUpDisableFlags(int state1, int state2) {
+        StatusBarSimpleFragment.assertInLegacyMode();
         mCommandQueue.disable(mDisplayId, state1, state2, false /* animate */);
     }
 
     // TODO(b/117478341): This was left such that CarStatusBar can override this method.
     // Try to remove this.
     protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
+        StatusBarSimpleFragment.assertInLegacyMode();
         mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
     }
 
@@ -1697,14 +1738,16 @@
     @Override
     public void checkBarModes() {
         if (mDemoModeController.isInDemoMode()) return;
-        if (mStatusBarTransitions != null) {
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator.
+        if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) {
             checkBarMode(
                     mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode().getValue(),
                     mStatusBarWindowState,
                     mStatusBarTransitions);
+            mNoAnimationOnNextBarModeChange = false;
         }
         mNavigationBarController.checkNavBarModes(mDisplayId);
-        mNoAnimationOnNextBarModeChange = false;
     }
 
     /** Temporarily hides Bubbles if the status bar is hidden. */
@@ -1728,7 +1771,9 @@
     }
 
     private void finishBarAnimations() {
-        if (mStatusBarTransitions != null) {
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator.
+        if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) {
             mStatusBarTransitions.finishAnimations();
         }
         mNavigationBarController.finishBarAnimations(mDisplayId);
@@ -1770,14 +1815,17 @@
         }
 
         pw.print("  mInteractingWindows="); pw.println(mInteractingWindows);
-        pw.print("  mStatusBarWindowState=");
-        pw.println(windowStateToString(mStatusBarWindowState));
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            pw.print("  mStatusBarWindowState=");
+            pw.println(windowStateToString(mStatusBarWindowState));
+        }
         pw.print("  mDozing="); pw.println(mDozing);
         pw.print("  mWallpaperSupported= "); pw.println(mWallpaperSupported);
 
-        CentralSurfaces.dumpBarTransitions(
-                pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
-
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            CentralSurfaces.dumpBarTransitions(
+                    pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
+        }
         pw.println("  mMediaManager: ");
         if (mMediaManager != null) {
             mMediaManager.dump(pw, args);
@@ -1850,7 +1898,11 @@
     private void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
         makeStatusBarView(result);
         mNotificationShadeWindowController.attach();
-        mStatusBarWindowController.attach();
+        // When the StatusBarSimpleFragment flag is enabled, this logic will be done in
+        // StatusBarOrchestrator
+        if (!StatusBarSimpleFragment.isEnabled()) {
+            mStatusBarWindowController.attach();
+        }
     }
 
     // called by makeStatusbar and also by PhoneStatusBarView
@@ -2475,7 +2527,7 @@
         int importance = bouncerShowing
                 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-        if (mPhoneStatusBarViewController != null) {
+        if (!StatusBarSimpleFragment.isEnabled() && mPhoneStatusBarViewController != null) {
             mPhoneStatusBarViewController.setImportantForAccessibility(importance);
         }
         mShadeSurface.setImportantForAccessibility(importance);
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 ee96195..93db2db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -20,6 +20,7 @@
 import static android.service.notification.NotificationListenerService.REASON_CLICK;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions;
+import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -112,6 +113,8 @@
                 boolean showOverTheLockScreen);
     }
 
+    private final static String TAG = "StatusBarNotificationActivityStarter";
+
     private final Context mContext;
     private final int mDisplayId;
 
@@ -229,6 +232,7 @@
      */
     @Override
     public void onNotificationBubbleIconClicked(NotificationEntry entry) {
+        expectNotNull(TAG, "entry", entry);
         Runnable action = () -> {
             mBubblesManagerOptional.ifPresent(bubblesManager ->
                     bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
@@ -255,6 +259,8 @@
      */
     @Override
     public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
+        expectNotNull(TAG, "entry", entry);
+        expectNotNull(TAG, "row", row);
         mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
                 mKeyguardStateController.isVisible(),
                 mNotificationShadeWindowController.getPanelExpanded());
@@ -437,6 +443,7 @@
      */
     @Override
     public void onDragSuccess(NotificationEntry entry) {
+        expectNotNull(TAG, "entry", entry);
         // this method is not responsible for intent sending.
         // will focus follow operation only after drag-and-drop that notification.
         final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
@@ -529,6 +536,8 @@
     @Override
     public void startNotificationGutsIntent(final Intent intent, final int appUid,
             ExpandableNotificationRow row) {
+        expectNotNull(TAG, "intent", intent);
+        expectNotNull(TAG, "row", row);
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
index 13b651e8..5b03198 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
@@ -16,10 +16,20 @@
 package com.android.systemui.statusbar.phone.dagger
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.core.CommandQueueInitializer
 import com.android.systemui.statusbar.core.StatusBarInitializer
 import com.android.systemui.statusbar.core.StatusBarInitializerImpl
+import com.android.systemui.statusbar.core.StatusBarOrchestrator
+import com.android.systemui.statusbar.core.StatusBarSimpleFragment
+import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStoreImpl
 import dagger.Binds
+import dagger.Lazy
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
 
@@ -27,6 +37,16 @@
 @Module
 interface StatusBarPhoneModule {
 
+    @Binds
+    abstract fun windowStateRepoStore(
+        impl: StatusBarWindowStateRepositoryStoreImpl
+    ): StatusBarWindowStateRepositoryStore
+
+    @Binds
+    abstract fun commandQCallbacks(
+        impl: CentralSurfacesCommandQueueCallbacks
+    ): CommandQueue.Callbacks
+
     /** Binds {@link StatusBarInitializer} as a {@link CoreStartable}. */
     @Binds
     @IntoMap
@@ -34,4 +54,34 @@
     fun bindStatusBarInitializer(impl: StatusBarInitializerImpl): CoreStartable
 
     @Binds fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        @IntoMap
+        @ClassKey(StatusBarOrchestrator::class)
+        fun orchestratorCoreStartable(
+            orchestratorLazy: Lazy<StatusBarOrchestrator>
+        ): CoreStartable {
+            return if (StatusBarSimpleFragment.isEnabled) {
+                orchestratorLazy.get()
+            } else {
+                CoreStartable.NOP
+            }
+        }
+
+        @Provides
+        @SysUISingleton
+        @IntoMap
+        @ClassKey(CommandQueueInitializer::class)
+        fun commandQueueInitializerCoreStartable(
+            initializerLazy: Lazy<CommandQueueInitializer>
+        ): CoreStartable {
+            return if (StatusBarSimpleFragment.isEnabled) {
+                initializerLazy.get()
+            } else {
+                CoreStartable.NOP
+            }
+        }
+    }
 }
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 78edd39..a1d5cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.lifecycle.DefaultLifecycleObserver
@@ -102,7 +103,13 @@
         val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
         PlatformTheme(isDarkTheme = cachedDarkTheme) {
             AlertDialogContent(
-                modifier = Modifier.semantics { testTagsAsResourceId = true },
+                modifier =
+                    Modifier.semantics {
+                        testTagsAsResourceId = true
+                        paneTitle = dialog.context.getString(
+                            R.string.accessibility_desc_quick_settings
+                        )
+                    },
                 title = {
                     Text(
                         modifier = Modifier.testTag("modes_title"),
@@ -137,7 +144,7 @@
         }
         activityStarter.startActivity(
             ZEN_MODE_SETTINGS_INTENT,
-            true /* dismissShade */,
+            /* dismissShade= */ true,
             animationController,
         )
     }
@@ -181,7 +188,7 @@
         if (animationController == null) {
             currentDialog?.dismiss()
         }
-        activityStarter.startActivity(intent, true, /* dismissShade */ animationController)
+        activityStarter.startActivity(intent, /* dismissShade= */ true, animationController)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt
similarity index 67%
rename from packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt
rename to packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt
index 298dacd..1c760be 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.util.kotlin
 
+import android.util.Log
 import java.util.Optional
 
 /**
@@ -28,3 +29,14 @@
  */
 @Suppress("NOTHING_TO_INLINE")
 inline fun <T> Optional<T>.getOrNull(): T? = orElse(null)
+
+/**
+ * Utility method to check if a value that is technically nullable is actually null. If it is null,
+ * this will crash development builds (but just log on production/droidfood builds). It can be used
+ * as a first step to verify if a nullable value can be made non-nullable instead.
+ */
+fun <T> expectNotNull(logTag: String, name: String, nullable: T?) {
+    if (nullable == null) {
+        Log.wtf(logTag, "Expected value of $name to not be null.")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
index 3c50c7b..09b1f45 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.wallet.controller
 
 import android.content.Intent
+import android.os.DeadObjectException
 import android.os.IBinder
 import android.util.Log
 import androidx.annotation.VisibleForTesting
@@ -47,7 +48,11 @@
                 controller.allWalletCards.collect { cards ->
                     val cardsSize = cards.size
                     Log.i(TAG, "Number of cards registered $cardsSize")
-                    listener?.registerNewWalletCards(cards)
+                    try {
+                        listener?.registerNewWalletCards(cards)
+                    } catch (e: DeadObjectException) {
+                        Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead")
+                    }
                 }
             }
         } else {
@@ -55,7 +60,11 @@
                 controller.allWalletCards.collect { cards ->
                     val cardsSize = cards.size
                     Log.i(TAG, "Number of cards registered $cardsSize")
-                    listener?.registerNewWalletCards(cards)
+                    try {
+                        listener?.registerNewWalletCards(cards)
+                    } catch (e: DeadObjectException) {
+                        Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead")
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index aa8c6b7..e160ff1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
@@ -643,6 +644,46 @@
         environment.verifyInputSessionDispose();
     }
 
+    @Test
+    public void testSessionPopAfterDestroy() {
+        final TouchHandler touchHandler = createTouchHandler();
+
+        final Environment environment = new Environment(Stream.of(touchHandler)
+                .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+
+        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(initialEvent);
+
+        // Ensure session started
+        final InputChannelCompat.InputEventListener eventListener =
+                registerInputEventListener(touchHandler);
+
+        // First event will be missed since we register after the execution loop,
+        final InputEvent event = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(event);
+        verify(eventListener).onInputEvent(eq(event));
+
+        final ArgumentCaptor<TouchHandler.TouchSession> touchSessionArgumentCaptor =
+                ArgumentCaptor.forClass(TouchHandler.TouchSession.class);
+
+        verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture());
+
+        environment.updateLifecycle(Lifecycle.State.DESTROYED);
+
+        // Check to make sure the input session is now disposed.
+        environment.verifyInputSessionDispose();
+
+        clearInvocations(environment.mInputFactory);
+
+        // Pop the session
+        touchSessionArgumentCaptor.getValue().pop();
+
+        environment.executeAll();
+
+        // Ensure no input sessions were created due to the session reset.
+        verifyNoMoreInteractions(environment.mInputFactory);
+    }
+
 
     @Test
     public void testPilfering() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 5cc6454..0d369a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -19,6 +19,7 @@
 import android.app.AlertDialog
 import android.platform.test.annotations.MotionTest
 import android.testing.TestableLooper.RunWithLooper
+import android.view.View
 import androidx.activity.BackEventCompat
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
@@ -52,27 +53,21 @@
 import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
 import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
-import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
 import com.android.systemui.scene.ui.composable.Scene
 import com.android.systemui.scene.ui.composable.SceneContainer
-import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
 import com.android.systemui.settings.displayTracker
-import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.awaitCancellation
@@ -85,6 +80,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
 import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
 import platform.test.motion.compose.ComposeRecordingSpec
 import platform.test.motion.compose.MotionControl
@@ -121,24 +117,17 @@
         val navigationDistances = mapOf(Scenes.Lockscreen to 1, Scenes.Bouncer to 0)
         SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
     }
+    private val view = mock<View>()
 
     private val transitionState by lazy {
         MutableStateFlow<ObservableTransitionState>(
             ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
         )
     }
+
     private val sceneContainerViewModel by lazy {
-        SceneContainerViewModel(
-                sceneInteractor = kosmos.sceneInteractor,
-                falsingInteractor = kosmos.falsingInteractor,
-                powerInteractor = kosmos.powerInteractor,
-                shadeInteractor = kosmos.shadeInteractor,
-                splitEdgeDetector = kosmos.splitEdgeDetector,
-                logger = kosmos.sceneLogger,
-                gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
-                displayId = kosmos.displayTracker.defaultDisplayId,
-                motionEventHandlerReceiver = {},
-            )
+        kosmos.sceneContainerViewModelFactory
+            .create(view, kosmos.displayTracker.defaultDisplayId, {})
             .apply { setTransitionState(transitionState) }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b0810a9..6608542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -100,6 +100,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -199,6 +200,7 @@
     private @Mock ShadeWindowLogger mShadeWindowLogger;
     private @Mock SelectedUserInteractor mSelectedUserInteractor;
     private @Mock KeyguardInteractor mKeyguardInteractor;
+    private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
     private @Captor ArgumentCaptor<KeyguardStateController.Callback>
             mKeyguardStateControllerCallback;
     private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -1294,6 +1296,7 @@
                 () -> mock(WindowManagerLockscreenVisibilityManager.class),
                 mSelectedUserInteractor,
                 mKeyguardInteractor,
+                mKeyguardTransitionBootInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
index 8a5af09..ad5eeab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -65,7 +65,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     private val goneToLs =
@@ -75,7 +75,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     @Before
@@ -84,7 +84,8 @@
         kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
         testScope.launch {
             kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff(
-                KeyguardState.LOCKSCREEN
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
             )
         }
     }
@@ -105,11 +106,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertTransition(
@@ -142,11 +139,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -191,7 +184,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
             sceneTransitions.value = lsToGone
@@ -205,11 +198,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -257,11 +246,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -297,7 +282,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -330,11 +315,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             val stepM3 = allSteps[allSteps.size - 3]
@@ -393,7 +374,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -466,7 +447,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -523,7 +504,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -577,11 +558,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -589,7 +566,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -641,11 +618,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -653,7 +626,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -702,11 +675,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -714,7 +683,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -736,7 +705,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -777,7 +746,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -799,7 +768,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
             allSteps[allSteps.size - 3]
 
@@ -858,7 +827,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -880,7 +849,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -959,7 +928,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -977,7 +946,7 @@
                     from = KeyguardState.AOD,
                     to = KeyguardState.DOZING,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -995,7 +964,7 @@
                     from = KeyguardState.DOZING,
                     to = KeyguardState.OCCLUDED,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -1017,7 +986,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1077,7 +1046,7 @@
                         from = KeyguardState.LOCKSCREEN,
                         to = KeyguardState.AOD,
                         animator = null,
-                        modeOnCanceled = TransitionModeOnCanceled.RESET
+                        modeOnCanceled = TransitionModeOnCanceled.RESET,
                     )
                 )
 
@@ -1092,7 +1061,7 @@
             kosmos.realKeyguardTransitionRepository.updateTransition(
                 ktfUuid!!,
                 1f,
-                TransitionState.FINISHED
+                TransitionState.FINISHED,
             )
 
             assertTransition(
@@ -1110,7 +1079,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1171,7 +1140,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1235,7 +1204,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1291,7 +1260,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1308,7 +1277,7 @@
         from: KeyguardState? = null,
         to: KeyguardState? = null,
         state: TransitionState? = null,
-        progress: Float? = null
+        progress: Float? = null,
     ) {
         if (from != null) assertThat(step.from).isEqualTo(from)
         if (to != null) assertThat(step.to).isEqualTo(to)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index d32d8cc..fb376ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -1890,7 +1890,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1911,7 +1911,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1928,7 +1928,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1939,7 +1939,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1983,7 +1983,7 @@
             backgroundExecutor.runAllReady()
             foregroundExecutor.runAllReady()
 
-            stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+            onStateUpdated(PACKAGE_NAME, state)
 
             verify(listener)
                 .onMediaDataLoaded(
@@ -2008,7 +2008,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2518,4 +2518,10 @@
                 eq(false),
             )
     }
+
+    private fun onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        backgroundExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 90af932..7d364bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -1967,7 +1967,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1988,7 +1988,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2005,7 +2005,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2016,7 +2016,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2059,7 +2059,7 @@
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
 
-        stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+        testScope.onStateUpdated(PACKAGE_NAME, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2084,7 +2084,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2603,4 +2603,11 @@
                 eq(false),
             )
     }
+
+    /** Helper function to update state and run executors */
+    private fun TestScope.onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        runCurrent()
+        advanceUntilIdle()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 03667cf..570c640 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -21,19 +21,19 @@
 import android.content.res.Configuration
 import android.database.ContentObserver
 import android.os.LocaleList
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
 import android.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.internal.logging.InstanceId
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.DisableSceneContainer
@@ -71,7 +71,6 @@
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.testKosmos
-import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
@@ -106,6 +105,8 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 private val DATA = MediaTestUtils.emptyMediaData
 
@@ -116,8 +117,8 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testDispatcher = kosmos.unconfinedTestDispatcher
     private val secureSettings = kosmos.unconfinedDispatcherFakeSettings
@@ -129,7 +130,6 @@
     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
     @Mock lateinit var mediaHostState: MediaHostState
     @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
     @Mock lateinit var mediaDataManager: MediaDataManager
     @Mock lateinit var configurationController: ConfigurationController
     @Mock lateinit var falsingManager: FalsingManager
@@ -153,16 +153,33 @@
 
     private val clock = FakeSystemClock()
     private lateinit var bgExecutor: FakeExecutor
+    private lateinit var uiExecutor: FakeExecutor
     private lateinit var mediaCarouselController: MediaCarouselController
 
     private var originalResumeSetting =
         Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.progressionOf(
+                com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND
+            )
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
         bgExecutor = FakeExecutor(clock)
+        uiExecutor = FakeExecutor(clock)
+
         mediaCarouselController =
             MediaCarouselController(
                 applicationScope = kosmos.applicationCoroutineScope,
@@ -173,7 +190,7 @@
                 activityStarter = activityStarter,
                 systemClock = clock,
                 mainDispatcher = kosmos.testDispatcher,
-                executor = executor,
+                uiExecutor = uiExecutor,
                 bgExecutor = bgExecutor,
                 backgroundDispatcher = testDispatcher,
                 mediaManager = mediaDataManager,
@@ -201,10 +218,11 @@
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
         FakeExecutor.exhaustExecutors(bgExecutor)
+        FakeExecutor.exhaustExecutors(uiExecutor)
         verify(globalSettings)
             .registerContentObserverSync(
                 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
-                capture(settingsObserverCaptor)
+                capture(settingsObserverCaptor),
             )
     }
 
@@ -213,7 +231,7 @@
         Settings.Secure.putInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RESUME,
-            originalResumeSetting
+            originalResumeSetting,
         )
     }
 
@@ -227,9 +245,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                4500L
+                4500L,
             )
 
         val playingCast =
@@ -239,9 +257,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedLocal =
@@ -251,9 +269,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                1000L
+                1000L,
             )
 
         val pausedCast =
@@ -263,9 +281,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                2000L
+                2000L,
             )
 
         val playingRcn =
@@ -275,9 +293,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedRcn =
@@ -287,9 +305,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val active =
@@ -299,9 +317,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                250L
+                250L,
             )
 
         val resume1 =
@@ -311,9 +329,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                500L
+                500L,
             )
 
         val resume2 =
@@ -323,9 +341,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                1000L
+                1000L,
             )
 
         val activeMoreRecent =
@@ -336,9 +354,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 2L
+                    lastActive = 2L,
                 ),
-                1000L
+                1000L,
             )
 
         val activeLessRecent =
@@ -349,9 +367,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 1L
+                    lastActive = 1L,
                 ),
-                1000L
+                1000L,
             )
         // Expected ordering for media players:
         // Actively playing local sessions
@@ -370,7 +388,7 @@
                 pausedRcn,
                 active,
                 resume2,
-                resume1
+                resume1,
             )
 
         expected.forEach {
@@ -380,7 +398,7 @@
                 it.second.copy(notificationKey = it.first),
                 panel,
                 clock,
-                isSsReactivated = false
+                isSsReactivated = false,
             )
         }
 
@@ -403,7 +421,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             true,
-            clock
+            clock,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -421,7 +439,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -439,7 +457,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             false,
-            clock
+            clock,
         )
 
         // Then it should be shown at the end of the carousel's active entries
@@ -461,8 +479,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -471,19 +489,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         // paused player order should stays the same in visibleMediaPLayer map.
         // paused player order should be first in mediaPlayer map.
         assertEquals(
             MediaPlayerData.visiblePlayerKeys().elementAt(3),
-            MediaPlayerData.playerKeys().elementAt(0)
+            MediaPlayerData.playerKeys().elementAt(0),
         )
     }
 
@@ -506,7 +525,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             LOCATION_QS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(LOCATION_QS)
@@ -517,7 +536,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_QQS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
@@ -528,7 +547,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_LOCKSCREEN,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -539,7 +558,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
@@ -570,8 +589,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -580,14 +599,15 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
         // adding a media recommendation card.
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
         mediaCarouselController.shouldScrollToKey = true
         // switching between media players.
@@ -598,8 +618,8 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -608,13 +628,14 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -626,7 +647,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
-            false
+            false,
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -635,14 +656,15 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(
             playerIndex,
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         assertEquals(playerIndex, 0)
 
@@ -657,9 +679,10 @@
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
                 resumption = false,
-                packageName = "PACKAGE_NAME"
-            )
+                packageName = "PACKAGE_NAME",
+            ),
         )
+        runAllReady()
         playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(playerIndex, 0)
     }
@@ -704,7 +727,7 @@
             player1.second.copy(notificationKey = player1.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
@@ -717,7 +740,7 @@
             player2.second.copy(notificationKey = player2.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -732,7 +755,7 @@
             player3.second.copy(notificationKey = player3.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -822,7 +845,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then the carousel is updated
@@ -841,7 +864,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
 
         // Then it is added to the carousel with correct state
@@ -886,7 +909,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.GONE,
-                this
+                this,
             )
 
             verify(mediaCarousel).visibility = View.VISIBLE
@@ -932,7 +955,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -961,7 +984,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -1125,12 +1148,14 @@
         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
 
         // This is processed as a user-initiated dismissal
         verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true))
@@ -1148,12 +1173,14 @@
 
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can't be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
         visualStabilityCallback.value.onReorderingAllowed()
 
         // This is processed as a user-initiated dismissal
@@ -1175,8 +1202,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -1185,18 +1212,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         val playersSize = MediaPlayerData.players().size
         reset(pageIndicator)
         function()
+        runAllReady()
 
         assertEquals(playersSize, MediaPlayerData.players().size)
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -1225,4 +1254,11 @@
         )
         runCurrent()
     }
+
+    private fun runAllReady() {
+        if (mediaControlsUmoInflationInBackground()) {
+            bgExecutor.runAllReady()
+            uiExecutor.runAllReady()
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index a6afd0e..f5a90196 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -22,10 +22,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -40,6 +36,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
 import android.animation.Animator;
 import android.annotation.IdRes;
 import android.content.ContentResolver;
@@ -95,6 +95,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.haptics.msdl.FakeMSDLPlayer;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewConfigurator;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository;
@@ -151,6 +152,7 @@
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
 import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository;
@@ -167,7 +169,6 @@
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -200,12 +201,6 @@
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
-import dagger.Lazy;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.channels.BufferOverflow;
-import kotlinx.coroutines.test.TestScope;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -220,6 +215,11 @@
 import java.util.List;
 import java.util.Optional;
 
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.channels.BufferOverflow;
+import kotlinx.coroutines.test.TestScope;
+
 public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
 
     protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -374,6 +374,7 @@
     protected View.OnLayoutChangeListener mLayoutChangeListener;
     protected KeyguardStatusViewController mKeyguardStatusViewController;
     protected ShadeRepository mShadeRepository;
+    protected FakeMSDLPlayer mMSDLPlayer = mKosmos.getMsdlPlayer();
 
     protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
     protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
@@ -761,7 +762,8 @@
                 new ResourcesSplitShadeStateController(),
                 mPowerInteractor,
                 mKeyguardClockPositionAlgorithm,
-                mNaturalScrollingSettingObserver);
+                mNaturalScrollingSettingObserver,
+                mMSDLPlayer);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
                 null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index a7fd160..43dbb40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -49,6 +49,7 @@
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
+import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -69,6 +70,8 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
 
+import com.google.android.msdl.data.model.MSDLToken;
+
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -1458,4 +1461,23 @@
 
         assertThat(mNotificationPanelViewController.getFalsingThreshold()).isGreaterThan(14);
     }
+
+    @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK)
+    public void performHapticFeedback_withMSDL_forGestureStart_deliversDragThresholdToken() {
+        mNotificationPanelViewController
+                .performHapticFeedback(HapticFeedbackConstants.GESTURE_START);
+
+        assertThat(mMSDLPlayer.getLatestTokenPlayed())
+                .isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR);
+    }
+
+    @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK)
+    public void performHapticFeedback_withMSDL_forReject_deliversFailureToken() {
+        mNotificationPanelViewController
+                .performHapticFeedback(HapticFeedbackConstants.REJECT);
+
+        assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 90655c3..97441f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
-import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -157,6 +156,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun doubleTapRequired_onKeyguard_usesPerformHapticFeedback() = runTest {
         launch(Dispatchers.Main.immediate) {
             val listener = getFalsingTapListener()
@@ -184,6 +184,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun doubleTapRequired_shadeLocked_usesPerformHapticFeedback() = runTest {
         launch(Dispatchers.Main.immediate) {
             val listener = getFalsingTapListener()
@@ -209,7 +210,7 @@
                     KEYGUARD /*statusBarState*/,
                     false /*keyguardFadingAway*/,
                     false /*goingToFullShade*/,
-                    SHADE /*oldStatusBarState*/
+                    SHADE, /*oldStatusBarState*/
                 )
         }
         advanceUntilIdle()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt
new file mode 100644
index 0000000..2a196c6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.core
+
+import android.internal.statusbar.fakeStatusBarService
+import android.platform.test.annotations.EnableFlags
+import android.view.WindowInsets
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.initController
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockCommandQueueCallbacks
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
+
+@EnableFlags(StatusBarSimpleFragment.FLAG_NAME)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommandQueueInitializerTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val initController = kosmos.initController
+    private val commandQueue = kosmos.fakeCommandQueue
+    private val commandQueueCallbacks = kosmos.mockCommandQueueCallbacks
+    private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
+    private val fakeStatusBarService = kosmos.fakeStatusBarService
+    private val initializer = kosmos.commandQueueInitializer
+
+    @Test
+    fun start_registersStatusBar() {
+        initializer.start()
+
+        assertThat(fakeStatusBarService.registeredStatusBar).isNotNull()
+    }
+
+    @Test
+    fun start_barResultHasTransientStatusBar_transientStateIsTrue() {
+        fakeStatusBarService.transientBarTypes = WindowInsets.Type.statusBars()
+
+        initializer.start()
+
+        assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isTrue()
+    }
+
+    @Test
+    fun start_barResultDoesNotHaveTransientStatusBar_transientStateIsFalse() {
+        fakeStatusBarService.transientBarTypes = WindowInsets.Type.navigationBars()
+
+        initializer.start()
+
+        assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isFalse()
+    }
+
+    @Test
+    fun start_callsOnSystemBarAttributesChanged_basedOnRegisterBarResult() {
+        initializer.start()
+
+        verify(commandQueueCallbacks)
+            .onSystemBarAttributesChanged(
+                context.displayId,
+                fakeStatusBarService.appearance,
+                fakeStatusBarService.appearanceRegions,
+                fakeStatusBarService.navbarColorManagedByIme,
+                fakeStatusBarService.behavior,
+                fakeStatusBarService.requestedVisibleTypes,
+                fakeStatusBarService.packageName,
+                fakeStatusBarService.letterboxDetails,
+            )
+    }
+
+    @Test
+    fun start_callsSetIcon_basedOnRegisterBarResult() {
+        initializer.start()
+
+        assertThat(commandQueue.icons).isEqualTo(fakeStatusBarService.statusBarIcons)
+    }
+
+    @Test
+    fun start_callsSetImeWindowStatus_basedOnRegisterBarResult() {
+        initializer.start()
+
+        verify(commandQueueCallbacks)
+            .setImeWindowStatus(
+                context.displayId,
+                fakeStatusBarService.imeWindowVis,
+                fakeStatusBarService.imeBackDisposition,
+                fakeStatusBarService.showImeSwitcher,
+            )
+    }
+
+    @Test
+    fun start_afterPostInitTaskExecuted_callsDisableFlags_basedOnRegisterBarResult() {
+        initializer.start()
+
+        initController.executePostInitTasks()
+
+        assertThat(commandQueue.disableFlags1ForDisplay(context.displayId))
+            .isEqualTo(fakeStatusBarService.disabledFlags1)
+        assertThat(commandQueue.disableFlags2ForDisplay(context.displayId))
+            .isEqualTo(fakeStatusBarService.disabledFlags2)
+    }
+
+    @Test
+    fun start_beforePostInitTaskExecuted_doesNotCallsDisableFlags() {
+        initializer.start()
+
+        assertThat(commandQueue.disableFlags1ForDisplay(context.displayId)).isNull()
+        assertThat(commandQueue.disableFlags2ForDisplay(context.displayId)).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
new file mode 100644
index 0000000..5803365
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.core
+
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.mockPluginDependencyProvider
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shade.mockNotificationShadeWindowViewController
+import com.android.systemui.shade.mockShadeSurface
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT
+import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT_TRANSPARENT
+import com.android.systemui.statusbar.data.model.StatusBarMode.OPAQUE
+import com.android.systemui.statusbar.data.model.StatusBarMode.TRANSPARENT
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.phone.mockPhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.mockPhoneStatusBarViewController
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.statusbar.window.data.repository.fakeStatusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.fakeStatusBarWindowController
+import com.android.systemui.testKosmos
+import com.android.wm.shell.bubbles.bubbles
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@EnableFlags(StatusBarSimpleFragment.FLAG_NAME)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StatusBarOrchestratorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().also {
+            it.testDispatcher = it.unconfinedTestDispatcher
+            it.statusBarWindowStateRepositoryStore = it.fakeStatusBarWindowStateRepositoryStore
+        }
+    private val testScope = kosmos.testScope
+    private val statusBarViewController = kosmos.mockPhoneStatusBarViewController
+    private val statusBarWindowController = kosmos.fakeStatusBarWindowController
+    private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
+    private val pluginDependencyProvider = kosmos.mockPluginDependencyProvider
+    private val notificationShadeWindowViewController =
+        kosmos.mockNotificationShadeWindowViewController
+    private val shadeSurface = kosmos.mockShadeSurface
+    private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+    private val fakeStatusBarWindowStateRepositoryStore =
+        kosmos.fakeStatusBarWindowStateRepositoryStore
+    private val fakePowerRepository = kosmos.fakePowerRepository
+    private val mockPhoneStatusBarTransitions = kosmos.mockPhoneStatusBarTransitions
+    private val mockBubbles = kosmos.bubbles
+
+    private val orchestrator = kosmos.statusBarOrchestrator
+
+    @Test
+    fun start_setsUpPluginDependencies() {
+        orchestrator.start()
+
+        verify(pluginDependencyProvider).allowPluginDependency(DarkIconDispatcher::class.java)
+        verify(pluginDependencyProvider).allowPluginDependency(StatusBarStateController::class.java)
+    }
+
+    @Test
+    fun start_attachesWindow() {
+        orchestrator.start()
+
+        assertThat(statusBarWindowController.isAttached).isTrue()
+    }
+
+    @Test
+    fun start_setsStatusBarControllerOnShade() {
+        orchestrator.start()
+
+        verify(notificationShadeWindowViewController)
+            .setStatusBarViewController(statusBarViewController)
+    }
+
+    @Test
+    fun start_updatesShadeExpansion() {
+        orchestrator.start()
+
+        verify(shadeSurface).updateExpansionAndVisibility()
+    }
+
+    @Test
+    fun bouncerShowing_setsImportanceForA11yToNoHideDescendants() =
+        testScope.runTest {
+            orchestrator.start()
+
+            bouncerRepository.setPrimaryShow(isShowing = true)
+
+            verify(statusBarViewController)
+                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
+        }
+
+    @Test
+    fun bouncerNotShowing_setsImportanceForA11yToNoHideDescendants() =
+        testScope.runTest {
+            orchestrator.start()
+
+            bouncerRepository.setPrimaryShow(isShowing = false)
+
+            verify(statusBarViewController)
+                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
+        }
+
+    @Test
+    fun deviceGoesToSleep_barTransitionsAnimationsAreFinished() =
+        testScope.runTest {
+            putDeviceToSleep()
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions).finishAnimations()
+        }
+
+    @Test
+    fun deviceIsAwake_barTransitionsAnimationsAreNotFinished() =
+        testScope.runTest {
+            awakeDevice()
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions, never()).finishAnimations()
+        }
+
+    @Test
+    fun statusBarVisible_notifiesBubbles() =
+        testScope.runTest {
+            setStatusBarMode(TRANSPARENT)
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+
+            orchestrator.start()
+
+            verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ true)
+        }
+
+    @Test
+    fun statusBarInLightsOutMode_notifiesBubblesWithStatusBarInvisible() =
+        testScope.runTest {
+            setStatusBarMode(LIGHTS_OUT)
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+
+            orchestrator.start()
+
+            verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+        }
+
+    @Test
+    fun statusBarInLightsOutTransparentMode_notifiesBubblesWithStatusBarInvisible() =
+        testScope.runTest {
+            setStatusBarMode(LIGHTS_OUT_TRANSPARENT)
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+
+            orchestrator.start()
+
+            verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+        }
+
+    @Test
+    fun statusBarWindowNotShowing_notifiesBubblesWithStatusBarInvisible() =
+        testScope.runTest {
+            setStatusBarMode(TRANSPARENT)
+            setStatusBarWindowState(StatusBarWindowState.Hidden)
+
+            orchestrator.start()
+
+            verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false)
+        }
+
+    @Test
+    fun statusBarModeChange_transitionsToModeWithAnimation() =
+        testScope.runTest {
+            awakeDevice()
+            clearTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+        }
+
+    @Test
+    fun statusBarModeChange_keepsTransitioningAsModeChanges() =
+        testScope.runTest {
+            awakeDevice()
+            clearTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+
+            setStatusBarMode(OPAQUE)
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(OPAQUE.toTransitionModeInt(), /* animate= */ true)
+
+            setStatusBarMode(LIGHTS_OUT)
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(LIGHTS_OUT.toTransitionModeInt(), /* animate= */ true)
+
+            setStatusBarMode(LIGHTS_OUT_TRANSPARENT)
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(LIGHTS_OUT_TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+        }
+
+    @Test
+    fun statusBarModeChange_transientIsShown_transitionsToModeWithoutAnimation() =
+        testScope.runTest {
+            awakeDevice()
+            setTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+        }
+
+    @Test
+    fun statusBarModeChange_windowIsHidden_transitionsToModeWithoutAnimation() =
+        testScope.runTest {
+            awakeDevice()
+            clearTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Hidden)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+        }
+
+    @Test
+    fun statusBarModeChange_deviceIsAsleep_transitionsToModeWithoutAnimation() =
+        testScope.runTest {
+            putDeviceToSleep()
+            clearTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            verify(mockPhoneStatusBarTransitions)
+                .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false)
+        }
+
+    @Test
+    fun statusBarModeAnimationConditionsChange_withoutBarModeChange_noNewTransitionsHappen() =
+        testScope.runTest {
+            awakeDevice()
+            clearTransientStatusBar()
+            setStatusBarWindowState(StatusBarWindowState.Showing)
+            setStatusBarMode(TRANSPARENT)
+
+            orchestrator.start()
+
+            putDeviceToSleep()
+            awakeDevice()
+            setTransientStatusBar()
+            clearTransientStatusBar()
+
+            verify(mockPhoneStatusBarTransitions, times(1))
+                .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true)
+        }
+
+    private fun putDeviceToSleep() {
+        fakePowerRepository.updateWakefulness(
+            rawState = WakefulnessState.ASLEEP,
+            lastWakeReason = WakeSleepReason.KEY,
+            lastSleepReason = WakeSleepReason.KEY,
+            powerButtonLaunchGestureTriggered = true,
+        )
+    }
+
+    private fun awakeDevice() {
+        fakePowerRepository.updateWakefulness(
+            rawState = WakefulnessState.AWAKE,
+            lastWakeReason = WakeSleepReason.KEY,
+            lastSleepReason = WakeSleepReason.KEY,
+            powerButtonLaunchGestureTriggered = true,
+        )
+    }
+
+    private fun setTransientStatusBar() {
+        statusBarModeRepository.defaultDisplay.showTransient()
+    }
+
+    private fun clearTransientStatusBar() {
+        statusBarModeRepository.defaultDisplay.clearTransient()
+    }
+
+    private fun setStatusBarWindowState(state: StatusBarWindowState) {
+        fakeStatusBarWindowStateRepositoryStore.defaultDisplay.setWindowState(state)
+    }
+
+    private fun setStatusBarMode(statusBarMode: StatusBarMode) {
+        statusBarModeRepository.defaultDisplay.statusBarMode.value = statusBarMode
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index cea8857..7d5278e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -332,7 +332,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
 
             assertThat(showHeadsUpStatusBar).isFalse()
         }
@@ -345,7 +348,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
@@ -359,7 +365,10 @@
 
             // WHEN no pinned rows
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index c710c56..15ea811 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -169,6 +169,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.core.StatusBarInitializerImpl;
+import com.android.systemui.statusbar.core.StatusBarOrchestrator;
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -346,6 +347,7 @@
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
     @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
     @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
+    @Mock private StatusBarOrchestrator mStatusBarOrchestrator;
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index e396b56..0598b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -133,7 +133,10 @@
 
             // WHEN HUN displayed on the bypass lock screen
             headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true))
-            keyguardTransitionRepository.emitInitialStepsFromOff(KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
new file mode 100644
index 0000000..cc0597b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.internal.statusbar
+
+import android.app.Notification
+import android.content.ComponentName
+import android.graphics.Rect
+import android.graphics.drawable.Icon
+import android.hardware.biometrics.IBiometricContextListener
+import android.hardware.biometrics.IBiometricSysuiReceiver
+import android.hardware.biometrics.PromptInfo
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback
+import android.media.INearbyMediaDevicesProvider
+import android.media.MediaRoute2Info
+import android.net.Uri
+import android.os.Bundle
+import android.os.IBinder
+import android.os.UserHandle
+import android.util.ArrayMap
+import android.view.KeyEvent
+import com.android.internal.logging.InstanceId
+import com.android.internal.statusbar.IAddTileResultCallback
+import com.android.internal.statusbar.ISessionListener
+import com.android.internal.statusbar.IStatusBar
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.internal.statusbar.RegisterStatusBarResult
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.internal.view.AppearanceRegion
+import org.mockito.kotlin.mock
+
+class FakeStatusBarService : IStatusBarService.Stub() {
+
+    var registeredStatusBar: IStatusBar? = null
+        private set
+
+    var statusBarIcons =
+        ArrayMap<String, StatusBarIcon>().also {
+            it["slot1"] = mock<StatusBarIcon>()
+            it["slot2"] = mock<StatusBarIcon>()
+        }
+    var disabledFlags1 = 1234567
+    var appearance = 123
+    var appearanceRegions =
+        arrayOf(
+            AppearanceRegion(
+                /* appearance = */ 123,
+                /* bounds = */ Rect(/* left= */ 4, /* top= */ 3, /* right= */ 2, /* bottom= */ 1),
+            ),
+            AppearanceRegion(
+                /* appearance = */ 345,
+                /* bounds = */ Rect(/* left= */ 1, /* top= */ 2, /* right= */ 3, /* bottom= */ 4),
+            ),
+        )
+    var imeWindowVis = 987
+    var imeBackDisposition = 654
+    var showImeSwitcher = true
+    var disabledFlags2 = 7654321
+    var navbarColorManagedByIme = true
+    var behavior = 234
+    var requestedVisibleTypes = 345
+    var packageName = "fake.bar.ser.vice"
+    var transientBarTypes = 0
+    var letterboxDetails =
+        arrayOf(
+            LetterboxDetails(
+                /* letterboxInnerBounds = */ Rect(
+                    /* left= */ 5,
+                    /* top= */ 6,
+                    /* right= */ 7,
+                    /* bottom= */ 8,
+                ),
+                /* letterboxFullBounds = */ Rect(
+                    /* left= */ 1,
+                    /* top= */ 2,
+                    /* right= */ 3,
+                    /* bottom= */ 4,
+                ),
+                /* appAppearance = */ 123,
+            )
+        )
+
+    override fun expandNotificationsPanel() {}
+
+    override fun collapsePanels() {}
+
+    override fun togglePanel() {}
+
+    override fun disable(what: Int, token: IBinder, pkg: String) {
+        disableForUser(what, token, pkg, userId = 0)
+    }
+
+    override fun disableForUser(what: Int, token: IBinder, pkg: String, userId: Int) {}
+
+    override fun disable2(what: Int, token: IBinder, pkg: String) {
+        disable2ForUser(what, token, pkg, userId = 0)
+    }
+
+    override fun disable2ForUser(what: Int, token: IBinder, pkg: String, userId: Int) {}
+
+    override fun getDisableFlags(token: IBinder, userId: Int): IntArray {
+        return intArrayOf(disabledFlags1, disabledFlags2)
+    }
+
+    override fun setIcon(
+        slot: String,
+        iconPackage: String,
+        iconId: Int,
+        iconLevel: Int,
+        contentDescription: String,
+    ) {}
+
+    override fun setIconVisibility(slot: String, visible: Boolean) {}
+
+    override fun removeIcon(slot: String) {}
+
+    override fun setImeWindowStatus(
+        displayId: Int,
+        vis: Int,
+        backDisposition: Int,
+        showImeSwitcher: Boolean,
+    ) {}
+
+    override fun expandSettingsPanel(subPanel: String) {}
+
+    override fun registerStatusBar(callbacks: IStatusBar): RegisterStatusBarResult {
+        registeredStatusBar = callbacks
+        return RegisterStatusBarResult(
+            statusBarIcons,
+            disabledFlags1,
+            appearance,
+            appearanceRegions,
+            imeWindowVis,
+            imeBackDisposition,
+            showImeSwitcher,
+            disabledFlags2,
+            navbarColorManagedByIme,
+            behavior,
+            requestedVisibleTypes,
+            packageName,
+            transientBarTypes,
+            letterboxDetails,
+        )
+    }
+
+    override fun onPanelRevealed(clearNotificationEffects: Boolean, numItems: Int) {}
+
+    override fun onPanelHidden() {}
+
+    override fun clearNotificationEffects() {}
+
+    override fun onNotificationClick(key: String, nv: NotificationVisibility) {}
+
+    override fun onNotificationActionClick(
+        key: String,
+        actionIndex: Int,
+        action: Notification.Action,
+        nv: NotificationVisibility,
+        generatedByAssistant: Boolean,
+    ) {}
+
+    override fun onNotificationError(
+        pkg: String,
+        tag: String,
+        id: Int,
+        uid: Int,
+        initialPid: Int,
+        message: String,
+        userId: Int,
+    ) {}
+
+    override fun onClearAllNotifications(userId: Int) {}
+
+    override fun onNotificationClear(
+        pkg: String,
+        userId: Int,
+        key: String,
+        dismissalSurface: Int,
+        dismissalSentiment: Int,
+        nv: NotificationVisibility,
+    ) {}
+
+    override fun onNotificationVisibilityChanged(
+        newlyVisibleKeys: Array<NotificationVisibility>,
+        noLongerVisibleKeys: Array<NotificationVisibility>,
+    ) {}
+
+    override fun onNotificationExpansionChanged(
+        key: String,
+        userAction: Boolean,
+        expanded: Boolean,
+        notificationLocation: Int,
+    ) {}
+
+    override fun onNotificationDirectReplied(key: String) {}
+
+    override fun onNotificationSmartSuggestionsAdded(
+        key: String,
+        smartReplyCount: Int,
+        smartActionCount: Int,
+        generatedByAssistant: Boolean,
+        editBeforeSending: Boolean,
+    ) {}
+
+    override fun onNotificationSmartReplySent(
+        key: String,
+        replyIndex: Int,
+        reply: CharSequence,
+        notificationLocation: Int,
+        modifiedBeforeSending: Boolean,
+    ) {}
+
+    override fun onNotificationSettingsViewed(key: String) {}
+
+    override fun onNotificationBubbleChanged(key: String, isBubble: Boolean, flags: Int) {}
+
+    override fun onBubbleMetadataFlagChanged(key: String, flags: Int) {}
+
+    override fun hideCurrentInputMethodForBubbles(displayId: Int) {}
+
+    override fun grantInlineReplyUriPermission(
+        key: String,
+        uri: Uri,
+        user: UserHandle,
+        packageName: String,
+    ) {}
+
+    override fun clearInlineReplyUriPermissions(key: String) {}
+
+    override fun onNotificationFeedbackReceived(key: String, feedback: Bundle) {}
+
+    override fun onGlobalActionsShown() {}
+
+    override fun onGlobalActionsHidden() {}
+
+    override fun shutdown() {}
+
+    override fun reboot(safeMode: Boolean) {}
+
+    override fun restart() {}
+
+    override fun addTile(tile: ComponentName) {}
+
+    override fun remTile(tile: ComponentName) {}
+
+    override fun clickTile(tile: ComponentName) {}
+
+    override fun handleSystemKey(key: KeyEvent) {}
+
+    override fun getLastSystemKey(): Int {
+        return -1
+    }
+
+    override fun showPinningEnterExitToast(entering: Boolean) {}
+
+    override fun showPinningEscapeToast() {}
+
+    override fun showAuthenticationDialog(
+        promptInfo: PromptInfo,
+        sysuiReceiver: IBiometricSysuiReceiver,
+        sensorIds: IntArray,
+        credentialAllowed: Boolean,
+        requireConfirmation: Boolean,
+        userId: Int,
+        operationId: Long,
+        opPackageName: String,
+        requestId: Long,
+    ) {}
+
+    override fun onBiometricAuthenticated(modality: Int) {}
+
+    override fun onBiometricHelp(modality: Int, message: String) {}
+
+    override fun onBiometricError(modality: Int, error: Int, vendorCode: Int) {}
+
+    override fun hideAuthenticationDialog(requestId: Long) {}
+
+    override fun setBiometicContextListener(listener: IBiometricContextListener) {}
+
+    override fun setUdfpsRefreshRateCallback(callback: IUdfpsRefreshRateRequestCallback) {}
+
+    override fun showInattentiveSleepWarning() {}
+
+    override fun dismissInattentiveSleepWarning(animated: Boolean) {}
+
+    override fun startTracing() {}
+
+    override fun stopTracing() {}
+
+    override fun isTracing(): Boolean {
+        return false
+    }
+
+    override fun suppressAmbientDisplay(suppress: Boolean) {}
+
+    override fun requestTileServiceListeningState(componentName: ComponentName, userId: Int) {}
+
+    override fun requestAddTile(
+        componentName: ComponentName,
+        label: CharSequence,
+        icon: Icon,
+        userId: Int,
+        callback: IAddTileResultCallback,
+    ) {}
+
+    override fun cancelRequestAddTile(packageName: String) {}
+
+    override fun setNavBarMode(navBarMode: Int) {}
+
+    override fun getNavBarMode(): Int {
+        return -1
+    }
+
+    override fun registerSessionListener(sessionFlags: Int, listener: ISessionListener) {}
+
+    override fun unregisterSessionListener(sessionFlags: Int, listener: ISessionListener) {}
+
+    override fun onSessionStarted(sessionType: Int, instanceId: InstanceId) {}
+
+    override fun onSessionEnded(sessionType: Int, instanceId: InstanceId) {}
+
+    override fun updateMediaTapToTransferSenderDisplay(
+        displayState: Int,
+        routeInfo: MediaRoute2Info,
+        undoCallback: IUndoMediaTransferCallback,
+    ) {}
+
+    override fun updateMediaTapToTransferReceiverDisplay(
+        displayState: Int,
+        routeInfo: MediaRoute2Info,
+        appIcon: Icon,
+        appName: CharSequence,
+    ) {}
+
+    override fun registerNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {}
+
+    override fun unregisterNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {}
+
+    override fun showRearDisplayDialog(currentBaseState: Int) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt
new file mode 100644
index 0000000..1304161
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.internal.statusbar
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeStatusBarService by Kosmos.Fixture { FakeStatusBarService() }
+
+var Kosmos.statusBarService by Kosmos.Fixture { fakeStatusBarService }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt
new file mode 100644
index 0000000..39384fd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui
+
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockDemoModeController by Kosmos.Fixture { mock<DemoModeController>() }
+
+var Kosmos.demoModeController by Kosmos.Fixture { mockDemoModeController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt
new file mode 100644
index 0000000..13169e1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.initController by Kosmos.Fixture { InitController() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt
new file mode 100644
index 0000000..1c84133
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+val Kosmos.communalUserActionsViewModel by Fixture {
+    CommunalUserActionsViewModel(
+        deviceUnlockedInteractor = deviceUnlockedInteractor,
+        shadeInteractor = shadeInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
index 3a59f6a..601c145 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
@@ -18,6 +18,8 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.content.Context
+import androidx.collection.ArrayMap
+import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.statusbar.CommandQueue
@@ -31,6 +33,11 @@
     CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) {
     private val callbacks = mutableListOf<Callbacks>()
 
+    val icons = ArrayMap<String, StatusBarIcon>()
+
+    private val perDisplayDisableFlags1 = mutableMapOf<Int, Int>()
+    private val perDisplayDisableFlags2 = mutableMapOf<Int, Int>()
+
     override fun addCallback(callback: Callbacks) {
         callbacks.add(callback)
     }
@@ -44,6 +51,23 @@
     }
 
     fun callbackCount(): Int = callbacks.size
+
+    override fun setIcon(slot: String, icon: StatusBarIcon) {
+        icons[slot] = icon
+    }
+
+    override fun disable(displayId: Int, state1: Int, state2: Int, animate: Boolean) {
+        perDisplayDisableFlags1[displayId] = state1
+        perDisplayDisableFlags2[displayId] = state2
+    }
+
+    override fun disable(displayId: Int, state1: Int, state2: Int) {
+        disable(displayId, state1, state2, /* animate= */ false)
+    }
+
+    fun disableFlags1ForDisplay(displayId: Int) = perDisplayDisableFlags1[displayId]
+
+    fun disableFlags2ForDisplay(displayId: Int) = perDisplayDisableFlags2[displayId]
 }
 
 @Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a73c184..4d0e603 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -48,9 +48,8 @@
  * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior.
  */
 @SysUISingleton
-class FakeKeyguardTransitionRepository(
-    private val initInLockscreen: Boolean = true,
-) : KeyguardTransitionRepository {
+class FakeKeyguardTransitionRepository(private val initInLockscreen: Boolean = true) :
+    KeyguardTransitionRepository {
     private val _transitions =
         MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
@@ -63,7 +62,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.LOCKSCREEN,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -71,12 +70,7 @@
     init {
         // Seed with a FINISHED transition in OFF, same as the real repository.
         _transitions.tryEmit(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
 
         if (initInLockscreen) {
@@ -173,7 +167,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 0.5f
+                        value = 0.5f,
                     )
             )
             testScheduler.runCurrent()
@@ -184,7 +178,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 1f
+                        value = 1f,
                     )
             )
             testScheduler.runCurrent()
@@ -208,7 +202,7 @@
         this.sendTransitionStep(
             step = step,
             validateStep = validateStep,
-            ownerName = step.ownerName
+            ownerName = step.ownerName,
         )
     }
 
@@ -240,9 +234,9 @@
                 to = to,
                 value = value,
                 transitionState = transitionState,
-                ownerName = ownerName
+                ownerName = ownerName,
             ),
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ) {
         if (step.transitionState == TransitionState.STARTED) {
             _currentTransitionInfo.value =
@@ -273,7 +267,7 @@
     fun sendTransitionStepJava(
         coroutineScope: CoroutineScope,
         step: TransitionStep,
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ): Job {
         return coroutineScope.launch {
             sendTransitionStep(step = step, validateStep = validateStep)
@@ -283,7 +277,7 @@
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
-        validateSteps: Boolean = true
+        validateSteps: Boolean = true,
     ) {
         steps.forEach {
             sendTransitionStep(step = it, validateStep = validateSteps)
@@ -296,7 +290,7 @@
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
         tryEmitInitialStepsFromOff(to)
     }
 
@@ -318,14 +312,14 @@
                 1f,
                 TransitionState.FINISHED,
                 ownerName = "KeyguardTransitionRepository(boot)",
-            ),
+            )
         )
     }
 
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) = Unit
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index 38bc758..2f13ba4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -22,6 +22,7 @@
 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
 
@@ -38,5 +39,6 @@
             alternateBouncerInteractor = alternateBouncerInteractor,
             shadeInteractor = { shadeInteractor },
             keyguardInteractor = { keyguardInteractor },
+            sceneInteractor = { sceneInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
index 0c538ff..ab7ccb3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.os.fakeExecutorHandler
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.keyguardBlueprintViewModel by
@@ -25,5 +26,6 @@
         KeyguardBlueprintViewModel(
             fakeExecutorHandler,
             keyguardBlueprintInteractor,
+            keyguardTransitionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 38626a5..3c87106 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -47,6 +47,8 @@
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         alternateBouncerToLockscreenTransitionViewModel =
             alternateBouncerToLockscreenTransitionViewModel,
+        alternateBouncerToOccludedTransitionViewModel =
+            alternateBouncerToOccludedTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
@@ -69,9 +71,12 @@
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
         lockscreenToPrimaryBouncerTransitionViewModel =
             lockscreenToPrimaryBouncerTransitionViewModel,
+        occludedToAlternateBouncerTransitionViewModel =
+            occludedToAlternateBouncerTransitionViewModel,
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..2acd1b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.occludedToAlternateBouncerTransitionViewModel by Fixture {
+    OccludedToAlternateBouncerTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..5d62a0f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.offToLockscreenTransitionViewModel by Fixture {
+    OffToLockscreenTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt
new file mode 100644
index 0000000..9e2039e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.navigationbar
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockNavigationBarController by Kosmos.Fixture { mock<NavigationBarController>() }
+
+var Kosmos.navigationBarController by Kosmos.Fixture { mockNavigationBarController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt
new file mode 100644
index 0000000..f1388e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins
+
+import android.testing.LeakCheck
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.utils.leaks.FakePluginManager
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+
+val Kosmos.leakCheck by Kosmos.Fixture { LeakCheck() }
+
+val Kosmos.fakePluginManager by Kosmos.Fixture { FakePluginManager(leakCheck) }
+
+var Kosmos.pluginManager by Kosmos.Fixture { fakePluginManager }
+
+val Kosmos.pluginDependencyProvider by Kosmos.Fixture { PluginDependencyProvider { pluginManager } }
+
+val Kosmos.mockPluginDependencyProvider by Kosmos.Fixture { mock<PluginDependencyProvider>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index d37d8f3..dbb3e38 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -19,14 +19,15 @@
 import android.content.res.mainResources
 import androidx.lifecycle.LifecycleCoroutineScope
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
+import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
 import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModel
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.transition.largeScreenShadeInterpolator
 import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
-import com.android.systemui.statusbar.phone.keyguardBypassController
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 
 val Kosmos.qsFragmentComposeViewModelFactory by
@@ -41,11 +42,12 @@
                     footerActionsViewModelFactory,
                     footerActionsController,
                     sysuiStatusBarStateController,
-                    keyguardBypassController,
+                    deviceEntryInteractor,
                     disableFlagsRepository,
                     largeScreenShadeInterpolator,
                     configurationInteractor,
                     largeScreenHeaderHelper,
+                    tileSquishinessInteractor,
                     lifecycleScope,
                 )
             }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt
new file mode 100644
index 0000000..d9fad32
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.tileSquishinessRepository by Kosmos.Fixture { TileSquishinessRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 3f62b4d..546129f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -20,6 +20,9 @@
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.tileSquishinessViewModel
 
 val Kosmos.infiniteGridLayout by
-    Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
+    Kosmos.Fixture {
+        InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel, tileSquishinessViewModel)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt
new file mode 100644
index 0000000..23db70f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.data.repository.tileSquishinessRepository
+
+val Kosmos.tileSquishinessInteractor by
+    Kosmos.Fixture { TileSquishinessInteractor(tileSquishinessRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
index 40d2624..babbd50 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
@@ -27,6 +27,7 @@
             currentTilesInteractor,
             fixedColumnsSizeViewModel,
             quickQuickSettingsRowInteractor,
+            tileSquishinessViewModel,
             iconTilesViewModel,
             applicationCoroutineScope,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt
new file mode 100644
index 0000000..ecc8cd1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
+
+val Kosmos.tileSquishinessViewModel by
+    Kosmos.Fixture { TileSquishinessViewModel(tileSquishinessInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 737aaf2..f842db4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -1,7 +1,9 @@
 package com.android.systemui.scene
 
+import android.view.View
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.haptics.msdl.msdlPlayer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -13,11 +15,13 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.FakeOverlay
 import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
+import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
 import com.android.systemui.settings.displayTracker
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import kotlinx.coroutines.flow.MutableStateFlow
+import org.mockito.kotlin.mock
 
 var Kosmos.sceneKeys by Fixture {
     listOf(
@@ -68,18 +72,32 @@
 }
 
 val Kosmos.sceneContainerViewModel by Fixture {
-    SceneContainerViewModel(
-            sceneInteractor = sceneInteractor,
-            falsingInteractor = falsingInteractor,
-            powerInteractor = powerInteractor,
-            shadeInteractor = shadeInteractor,
-            splitEdgeDetector = splitEdgeDetector,
-            gestureFilterFactory = sceneContainerGestureFilterFactory,
-            displayId = displayTracker.defaultDisplayId,
-            motionEventHandlerReceiver = {},
-            logger = sceneLogger,
-        )
-        .apply { setTransitionState(transitionState) }
+    sceneContainerViewModelFactory.create(mock<View>(), displayTracker.defaultDisplayId, {}).apply {
+        setTransitionState(transitionState)
+    }
+}
+
+val Kosmos.sceneContainerViewModelFactory by Fixture {
+    object : SceneContainerViewModel.Factory {
+        override fun create(
+            view: View,
+            displayId: Int,
+            motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit,
+        ): SceneContainerViewModel =
+            SceneContainerViewModel(
+                sceneInteractor = sceneInteractor,
+                falsingInteractor = falsingInteractor,
+                powerInteractor = powerInteractor,
+                shadeInteractor = shadeInteractor,
+                splitEdgeDetector = splitEdgeDetector,
+                logger = sceneLogger,
+                gestureFilterFactory = sceneContainerGestureFilterFactory,
+                hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
+                view = view,
+                displayId = displayId,
+                motionEventHandlerReceiver = motionEventHandlerReceiver,
+            )
+    }
 }
 
 val Kosmos.sceneContainerGestureFilterFactory by Fixture {
@@ -92,3 +110,16 @@
         }
     }
 }
+
+val Kosmos.sceneContainerHapticsViewModelFactory by Fixture {
+    object : SceneContainerHapticsViewModel.Factory {
+        override fun create(view: View): SceneContainerHapticsViewModel {
+            return SceneContainerHapticsViewModel(
+                view = view,
+                sceneInteractor = sceneInteractor,
+                shadeInteractor = shadeInteractor,
+                msdlPlayer = msdlPlayer,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
index 1ceab68..a9f9c82 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
@@ -20,3 +20,13 @@
 import com.android.systemui.util.mockito.mock
 
 var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() }
+
+val Kosmos.mockNotificationShadeWindowViewController by
+    Kosmos.Fixture { mock<NotificationShadeWindowViewController>() }
+
+var Kosmos.notificationShadeWindowViewController by
+    Kosmos.Fixture { mockNotificationShadeWindowViewController }
+
+val Kosmos.mockShadeSurface by Kosmos.Fixture { mock<ShadeSurface>() }
+
+var Kosmos.shadeSurface by Kosmos.Fixture { mockShadeSurface }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
index 48c5121..0aeea4e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt
@@ -19,11 +19,13 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.qs.ui.adapter.qsSceneAdapter
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 
 val Kosmos.shadeUserActionsViewModel: ShadeUserActionsViewModel by Fixture {
     ShadeUserActionsViewModel(
         qsSceneAdapter = qsSceneAdapter,
         shadeInteractor = shadeInteractor,
+        sceneBackInteractor = sceneBackInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
index 27f7f68..f571c1b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt
@@ -19,4 +19,10 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.commandQueue by Kosmos.Fixture { mock<CommandQueue>() }
+val Kosmos.mockCommandQueue by Kosmos.Fixture { mock<CommandQueue>() }
+
+var Kosmos.commandQueue by Kosmos.Fixture { mockCommandQueue }
+
+val Kosmos.mockCommandQueueCallbacks by Kosmos.Fixture { mock<CommandQueue.Callbacks>() }
+
+var Kosmos.commandQueueCallbacks by Kosmos.Fixture { mockCommandQueue }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
index 554bdbe..d436cd4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
@@ -19,5 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notificationRemoteInputManager by
+val Kosmos.mockNotificationRemoteInputManager by
     Kosmos.Fixture { mock<NotificationRemoteInputManager>() }
+
+var Kosmos.notificationRemoteInputManager by Kosmos.Fixture { mockNotificationRemoteInputManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt
new file mode 100644
index 0000000..cba4e8e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.core
+
+import android.content.testableContext
+import android.internal.statusbar.fakeStatusBarService
+import com.android.systemui.initController
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.navigationbar.mockNavigationBarController
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockCommandQueueCallbacks
+
+var Kosmos.commandQueueInitializer by
+    Kosmos.Fixture {
+        CommandQueueInitializer(
+            testableContext,
+            fakeCommandQueue,
+            { mockCommandQueueCallbacks },
+            fakeStatusBarModeRepository,
+            initController,
+            fakeStatusBarService,
+            mockNavigationBarController,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt
new file mode 100644
index 0000000..edd6604
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.core
+
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewUpdatedListener
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
+
+class FakeStatusBarInitializer(
+    private val statusBarViewController: PhoneStatusBarViewController,
+    private val statusBarTransitions: PhoneStatusBarTransitions,
+) : StatusBarInitializer {
+
+    override var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
+        set(value) {
+            field = value
+            value?.onStatusBarViewUpdated(statusBarViewController, statusBarTransitions)
+        }
+
+    override fun initializeStatusBar() {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
new file mode 100644
index 0000000..d103200
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.core
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.phoneStatusBarTransitions
+import com.android.systemui.statusbar.phone.phoneStatusBarViewController
+
+val Kosmos.fakeStatusBarInitializer by
+    Kosmos.Fixture {
+        FakeStatusBarInitializer(phoneStatusBarViewController, phoneStatusBarTransitions)
+    }
+
+var Kosmos.statusBarInitializer by Kosmos.Fixture { fakeStatusBarInitializer }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
new file mode 100644
index 0000000..c53e44d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.core
+
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.mockDemoModeController
+import com.android.systemui.plugins.mockPluginDependencyProvider
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.mockNotificationShadeWindowViewController
+import com.android.systemui.shade.mockShadeSurface
+import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.mockNotificationRemoteInputManager
+import com.android.systemui.statusbar.phone.mockAutoHideController
+import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore
+import com.android.systemui.statusbar.window.fakeStatusBarWindowController
+import com.android.wm.shell.bubbles.bubblesOptional
+
+val Kosmos.statusBarOrchestrator by
+    Kosmos.Fixture {
+        StatusBarOrchestrator(
+            applicationCoroutineScope,
+            fakeStatusBarInitializer,
+            fakeStatusBarWindowController,
+            fakeStatusBarModeRepository,
+            mockDemoModeController,
+            mockPluginDependencyProvider,
+            mockAutoHideController,
+            mockNotificationRemoteInputManager,
+            { mockNotificationShadeWindowViewController },
+            mockShadeSurface,
+            bubblesOptional,
+            statusBarWindowStateRepositoryStore,
+            powerInteractor,
+            primaryBouncerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index a9e117a..237f7e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.occludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.offToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.kosmos.Kosmos
@@ -85,6 +86,7 @@
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToGoneTransitionViewModel = occludedToGoneTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
             primaryBouncerToLockscreenTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt
new file mode 100644
index 0000000..090ce31
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockAutoHideController by Kosmos.Fixture { mock<AutoHideController>() }
+
+var Kosmos.autoHideController by Kosmos.Fixture { mockAutoHideController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt
new file mode 100644
index 0000000..603ee08
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt
@@ -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.systemui.statusbar.phone
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.Mockito.mock
+
+val Kosmos.mockPhoneStatusBarViewController: PhoneStatusBarViewController by
+    Kosmos.Fixture { mock(PhoneStatusBarViewController::class.java) }
+
+var Kosmos.phoneStatusBarViewController by Kosmos.Fixture { mockPhoneStatusBarViewController }
+
+val Kosmos.mockPhoneStatusBarTransitions: PhoneStatusBarTransitions by
+    Kosmos.Fixture { mock(PhoneStatusBarTransitions::class.java) }
+
+var Kosmos.phoneStatusBarTransitions by Kosmos.Fixture { mockPhoneStatusBarTransitions }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt
new file mode 100644
index 0000000..528c9d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.fragments.FragmentHostManager
+import java.util.Optional
+
+class FakeStatusBarWindowController : StatusBarWindowController {
+
+    var isAttached = false
+        private set
+
+    override val statusBarHeight: Int = 0
+
+    override fun refreshStatusBarHeight() {}
+
+    override fun attach() {
+        isAttached = true
+    }
+
+    override fun addViewToWindow(view: View, layoutParams: ViewGroup.LayoutParams) {}
+
+    override val backgroundView: View
+        get() = throw NotImplementedError()
+
+    override val fragmentHostManager: FragmentHostManager
+        get() = throw NotImplementedError()
+
+    override fun wrapAnimationControllerIfInStatusBar(
+        rootView: View,
+        animationController: ActivityTransitionAnimator.Controller,
+    ): Optional<ActivityTransitionAnimator.Controller> = Optional.empty()
+
+    override fun setForceStatusBarVisible(forceStatusBarVisible: Boolean) {}
+
+    override fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
new file mode 100644
index 0000000..c198b35
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() }
+
+var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt
new file mode 100644
index 0000000..6532a7e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import android.view.Display
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeStatusBarWindowStateRepositoryStore : StatusBarWindowStateRepositoryStore {
+
+    private val perDisplayRepos = mutableMapOf<Int, FakeStatusBarWindowStatePerDisplayRepository>()
+
+    override val defaultDisplay: FakeStatusBarWindowStatePerDisplayRepository =
+        forDisplay(Display.DEFAULT_DISPLAY)
+
+    override fun forDisplay(displayId: Int): FakeStatusBarWindowStatePerDisplayRepository =
+        perDisplayRepos.computeIfAbsent(displayId) {
+            FakeStatusBarWindowStatePerDisplayRepository()
+        }
+}
+
+class FakeStatusBarWindowStatePerDisplayRepository : StatusBarWindowStatePerDisplayRepository {
+
+    private val _windowState = MutableStateFlow(StatusBarWindowState.Hidden)
+
+    override val windowState = _windowState.asStateFlow()
+
+    fun setWindowState(state: StatusBarWindowState) {
+        _windowState.value = state
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
index e2b7f5f..2205a3b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
@@ -21,6 +21,9 @@
 import com.android.systemui.settings.displayTracker
 import com.android.systemui.statusbar.commandQueue
 
+val Kosmos.fakeStatusBarWindowStateRepositoryStore by
+    Kosmos.Fixture { FakeStatusBarWindowStateRepositoryStore() }
+
 class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: Kosmos) :
     StatusBarWindowStatePerDisplayRepositoryFactory {
     override fun create(displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl {
@@ -32,7 +35,7 @@
     }
 }
 
-val Kosmos.statusBarWindowStateRepositoryStore by
+var Kosmos.statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore by
     Kosmos.Fixture {
         StatusBarWindowStateRepositoryStoreImpl(
             displayId = displayTracker.defaultDisplayId,
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
index 428eb57..b4b8715 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -84,10 +84,10 @@
         try {
             mOutputWriter = new PrintWriter(mOutputFile);
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
-        // Crete the "latest" symlink.
+        // Create the "latest" symlink.
         Path symlink = Paths.get(tmpdir, basename + "latest.csv");
         try {
             if (Files.exists(symlink)) {
@@ -96,7 +96,7 @@
             Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
 
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
         Log.i(TAG, "Test result stats file: " + mOutputFile);
diff --git a/ravenwood/runtime-jni/ravenwood_sysprop.cpp b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
index 4fb61b6..aafc426 100644
--- a/ravenwood/runtime-jni/ravenwood_sysprop.cpp
+++ b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
@@ -56,7 +56,7 @@
     if (key == nullptr || *key == '\0') return false;
     if (value == nullptr) value = "";
     bool read_only = !strncmp(key, "ro.", 3);
-    if (!read_only && strlen(value) >= PROP_VALUE_MAX) return -1;
+    if (!read_only && strlen(value) >= PROP_VALUE_MAX) return false;
 
     std::lock_guard lock(g_properties_lock);
     auto [it, success] = g_properties.emplace(key, value);
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 082459b..b6c8fc7 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -16,22 +16,35 @@
 
 package com.android.server.appfunctions;
 
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
+import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB;
+import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE;
+
 import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
-import android.app.appfunctions.AppFunctionService;
+import android.app.appfunctions.AppFunctionManager;
+import android.app.appfunctions.AppFunctionManagerHelper;
+import android.app.appfunctions.AppFunctionRuntimeMetadata;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.ExecuteAppFunctionResponse;
+import android.app.appfunctions.IAppFunctionEnabledCallback;
 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.AppSearchBatchResult;
 import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
 import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.PutDocumentsRequest;
 import android.app.appsearch.observer.DocumentChangeInfo;
 import android.app.appsearch.observer.ObserverCallback;
 import android.app.appsearch.observer.ObserverSpec;
@@ -40,12 +53,15 @@
 import android.content.Intent;
 import android.os.Binder;
 import android.os.CancellationSignal;
-import android.os.IBinder;
 import android.os.ICancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.ParcelableException;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
 import com.android.server.SystemService.TargetUser;
@@ -54,6 +70,7 @@
 
 import java.util.Objects;
 import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
 
 /** Implementation of the AppFunctionManagerService. */
 public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
@@ -64,6 +81,7 @@
     private final ServiceHelper mInternalServiceHelper;
     private final ServiceConfig mServiceConfig;
     private final Context mContext;
+    private final Object mLock = new Object();
 
     public AppFunctionManagerServiceImpl(@NonNull Context context) {
         this(
@@ -180,52 +198,214 @@
             return;
         }
 
-        var unused =
-                mCallerValidator
-                        .verifyCallerCanExecuteAppFunction(
-                                callingUid,
-                                callingPid,
-                                requestInternal.getCallingPackage(),
-                                targetPackageName,
-                                requestInternal.getClientRequest().getFunctionIdentifier())
-                        .thenAccept(
-                                canExecute -> {
-                                    if (!canExecute) {
-                                        safeExecuteAppFunctionCallback.onResult(
-                                                ExecuteAppFunctionResponse.newFailure(
-                                                        ExecuteAppFunctionResponse.RESULT_DENIED,
-                                                        "Caller does not have permission to execute"
-                                                                + " the appfunction",
-                                                        /* extras= */ null));
-                                        return;
-                                    }
-                                    Intent serviceIntent =
-                                            mInternalServiceHelper.resolveAppFunctionService(
-                                                    targetPackageName, targetUser);
-                                    if (serviceIntent == null) {
-                                        safeExecuteAppFunctionCallback.onResult(
-                                                ExecuteAppFunctionResponse.newFailure(
-                                                        ExecuteAppFunctionResponse
-                                                                .RESULT_INTERNAL_ERROR,
-                                                        "Cannot find the target service.",
-                                                        /* extras= */ null));
-                                        return;
-                                    }
-                                    bindAppFunctionServiceUnchecked(
-                                            requestInternal,
-                                            serviceIntent,
-                                            targetUser,
-                                            localCancelTransport,
-                                            safeExecuteAppFunctionCallback,
-                                            /* bindFlags= */ Context.BIND_AUTO_CREATE
-                                                    | Context.BIND_FOREGROUND_SERVICE);
-                                })
-                        .exceptionally(
-                                ex -> {
-                                    safeExecuteAppFunctionCallback.onResult(
-                                            mapExceptionToExecuteAppFunctionResponse(ex));
-                                    return null;
-                                });
+        mCallerValidator
+                .verifyCallerCanExecuteAppFunction(
+                        callingUid,
+                        callingPid,
+                        requestInternal.getCallingPackage(),
+                        targetPackageName,
+                        requestInternal.getClientRequest().getFunctionIdentifier())
+                .thenAccept(
+                        canExecute -> {
+                            if (!canExecute) {
+                                safeExecuteAppFunctionCallback.onResult(
+                                        ExecuteAppFunctionResponse.newFailure(
+                                                ExecuteAppFunctionResponse.RESULT_DENIED,
+                                                "Caller does not have permission to execute the"
+                                                        + " appfunction",
+                                                /* extras= */ null));
+                            }
+                        })
+                .thenCompose(
+                        isEnabled ->
+                                isAppFunctionEnabled(
+                                        requestInternal.getClientRequest().getFunctionIdentifier(),
+                                        requestInternal.getClientRequest().getTargetPackageName(),
+                                        getAppSearchManagerAsUser(requestInternal.getUserHandle()),
+                                        THREAD_POOL_EXECUTOR))
+                .thenAccept(
+                        isEnabled -> {
+                            if (!isEnabled) {
+                                throw new DisabledAppFunctionException(
+                                        "The app function is disabled");
+                            }
+                        })
+                .thenAccept(
+                        unused -> {
+                            Intent serviceIntent =
+                                    mInternalServiceHelper.resolveAppFunctionService(
+                                            targetPackageName, targetUser);
+                            if (serviceIntent == null) {
+                                safeExecuteAppFunctionCallback.onResult(
+                                        ExecuteAppFunctionResponse.newFailure(
+                                                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                                                "Cannot find the target service.",
+                                                /* extras= */ null));
+                                return;
+                            }
+                            bindAppFunctionServiceUnchecked(
+                                    requestInternal,
+                                    serviceIntent,
+                                    targetUser,
+                                    localCancelTransport,
+                                    safeExecuteAppFunctionCallback,
+                                    /* bindFlags= */ Context.BIND_AUTO_CREATE
+                                            | Context.BIND_FOREGROUND_SERVICE);
+                        })
+                .exceptionally(
+                        ex -> {
+                            safeExecuteAppFunctionCallback.onResult(
+                                    mapExceptionToExecuteAppFunctionResponse(ex));
+                            return null;
+                        });
+    }
+
+    private static AndroidFuture<Boolean> isAppFunctionEnabled(
+            @NonNull String functionIdentifier,
+            @NonNull String targetPackage,
+            @NonNull AppSearchManager appSearchManager,
+            @NonNull Executor executor) {
+        AndroidFuture<Boolean> future = new AndroidFuture<>();
+        AppFunctionManagerHelper.isAppFunctionEnabled(
+                functionIdentifier,
+                targetPackage,
+                appSearchManager,
+                executor,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(@NonNull Boolean result) {
+                        future.complete(result);
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        future.completeExceptionally(error);
+                    }
+                });
+        return future;
+    }
+
+    @Override
+    public void setAppFunctionEnabled(
+            @NonNull String callingPackage,
+            @NonNull String functionIdentifier,
+            @NonNull UserHandle userHandle,
+            @AppFunctionManager.EnabledState int enabledState,
+            @NonNull IAppFunctionEnabledCallback callback) {
+        try {
+            mCallerValidator.validateCallingPackage(callingPackage);
+        } catch (SecurityException e) {
+            reportException(callback, e);
+            return;
+        }
+        THREAD_POOL_EXECUTOR.execute(
+                () -> {
+                    try {
+                        // TODO(357551503): Instead of holding a global lock, hold a per-package
+                        //  lock.
+                        synchronized (mLock) {
+                            setAppFunctionEnabledInternalLocked(
+                                    callingPackage, functionIdentifier, userHandle, enabledState);
+                        }
+                        callback.onSuccess();
+                    } catch (Exception e) {
+                        Slog.e(TAG, "Error in setAppFunctionEnabled: ", e);
+                        reportException(callback, e);
+                    }
+                });
+    }
+
+    private static void reportException(
+            @NonNull IAppFunctionEnabledCallback callback, @NonNull Exception exception) {
+        try {
+            callback.onError(new ParcelableException(exception));
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to report the exception", e);
+        }
+    }
+
+    /**
+     * Sets the enabled status of a specified app function.
+     * <p>
+     * Required to hold a lock to call this function to avoid document changes during the process.
+     */
+    @WorkerThread
+    @GuardedBy("mLock")
+    private void setAppFunctionEnabledInternalLocked(
+            @NonNull String callingPackage,
+            @NonNull String functionIdentifier,
+            @NonNull UserHandle userHandle,
+            @AppFunctionManager.EnabledState int enabledState)
+            throws Exception {
+        AppSearchManager perUserAppSearchManager = getAppSearchManagerAsUser(userHandle);
+
+        if (perUserAppSearchManager == null) {
+            throw new IllegalStateException(
+                    "AppSearchManager not found for user:" + userHandle.getIdentifier());
+        }
+        SearchContext runtimeMetadataSearchContext =
+                new SearchContext.Builder(APP_FUNCTION_RUNTIME_METADATA_DB).build();
+
+        try (FutureAppSearchSession runtimeMetadataSearchSession =
+                new FutureAppSearchSessionImpl(
+                        perUserAppSearchManager,
+                        THREAD_POOL_EXECUTOR,
+                        runtimeMetadataSearchContext)) {
+            AppFunctionRuntimeMetadata existingMetadata =
+                    new AppFunctionRuntimeMetadata(
+                            getRuntimeMetadataGenericDocument(
+                                    callingPackage,
+                                    functionIdentifier,
+                                    runtimeMetadataSearchSession));
+            AppFunctionRuntimeMetadata.Builder newMetadata =
+                    new AppFunctionRuntimeMetadata.Builder(existingMetadata);
+            switch (enabledState) {
+                case AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> {
+                    newMetadata.setEnabled(null);
+                }
+                case APP_FUNCTION_STATE_ENABLED -> {
+                    newMetadata.setEnabled(true);
+                }
+                case APP_FUNCTION_STATE_DISABLED -> {
+                    newMetadata.setEnabled(false);
+                }
+                default ->
+                        throw new IllegalArgumentException(
+                                "Value of EnabledState is unsupported.");
+            }
+            AppSearchBatchResult<String, Void> putDocumentBatchResult =
+                    runtimeMetadataSearchSession
+                            .put(
+                                    new PutDocumentsRequest.Builder()
+                                            .addGenericDocuments(newMetadata.build())
+                                            .build())
+                            .get();
+            if (!putDocumentBatchResult.isSuccess()) {
+                throw new IllegalStateException("Failed writing updated doc to AppSearch due to "
+                        + putDocumentBatchResult);
+            }
+        }
+    }
+
+    @WorkerThread
+    @NonNull
+    private AppFunctionRuntimeMetadata getRuntimeMetadataGenericDocument(
+            @NonNull String packageName,
+            @NonNull String functionId,
+            @NonNull FutureAppSearchSession runtimeMetadataSearchSession)
+            throws Exception {
+        String documentId =
+                AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(packageName, functionId);
+        GetByDocumentIdRequest request =
+                new GetByDocumentIdRequest.Builder(APP_FUNCTION_RUNTIME_NAMESPACE)
+                        .addIds(documentId)
+                        .build();
+        AppSearchBatchResult<String, GenericDocument> result =
+                runtimeMetadataSearchSession.getByDocumentId(request).get();
+        if (result.isSuccess()) {
+            return new AppFunctionRuntimeMetadata((result.getSuccesses().get(documentId)));
+        }
+        throw new IllegalArgumentException("Function " + functionId + " does not exist");
     }
 
     private void bindAppFunctionServiceUnchecked(
@@ -302,24 +482,27 @@
         }
     }
 
+    private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) {
+        return mContext.createContextAsUser(userHandle, /* flags= */ 0)
+                .getSystemService(AppSearchManager.class);
+    }
+
     private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) {
         if (e instanceof CompletionException) {
             e = e.getCause();
         }
-
-        if (e instanceof AppSearchException) {
-            AppSearchException appSearchException = (AppSearchException) e;
-            return ExecuteAppFunctionResponse.newFailure(
+        int resultCode = ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR;
+        if (e instanceof AppSearchException appSearchException) {
+            resultCode =
                     mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(
-                            appSearchException.getResultCode()),
-                    appSearchException.getMessage(),
-                    /* extras= */ null);
+                            appSearchException.getResultCode());
+        } else if (e instanceof SecurityException) {
+            resultCode = ExecuteAppFunctionResponse.RESULT_DENIED;
+        } else if (e instanceof DisabledAppFunctionException) {
+            resultCode = ExecuteAppFunctionResponse.RESULT_DISABLED;
         }
-
         return ExecuteAppFunctionResponse.newFailure(
-                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
-                e.getMessage(),
-                /* extras= */ null);
+                resultCode, e.getMessage(), /* extras= */ null);
     }
 
     private int mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(int resultCode) {
@@ -434,4 +617,11 @@
             }
         }
     }
+
+    /** Throws when executing a disabled app function. */
+    private static class DisabledAppFunctionException extends RuntimeException {
+        private DisabledAppFunctionException(@NonNull String errorMessage) {
+            super(errorMessage);
+        }
+    }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
index 070a99d..ffca849 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
@@ -65,8 +65,7 @@
             @NonNull UserHandle userHandle,
             @NonNull RunServiceCallCallback<T> callback) {
         OneOffServiceConnection serviceConnection =
-                new OneOffServiceConnection(
-                        intent, bindFlags, userHandle, callback);
+                new OneOffServiceConnection(intent, bindFlags, userHandle, callback);
 
         return serviceConnection.bindAndRun();
     }
@@ -93,7 +92,7 @@
             boolean bindServiceResult =
                     mContext.bindServiceAsUser(mIntent, this, mFlags, mUserHandle);
 
-            if(!bindServiceResult) {
+            if (!bindServiceResult) {
                 safeUnbind();
             }
 
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index 5b271a3..7474df2 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -87,7 +87,7 @@
 # replaces 27510 with a row per notification
 27531 notification_visibility (key|3),(visibile|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1)
 # a notification emited noise, vibration, or light
-27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1)
+27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1),(mute_reason|1)
 # a notification was added to a autogroup
 27533 notification_autogrouped (key|3)
 # notification was removed from an autogroup
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index fbe593f..682eb76 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -25,6 +25,7 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -91,6 +92,7 @@
  * Monitors the health of packages on the system and notifies interested observers when packages
  * fail. On failure, the registered observer with the least user impacting mitigation will
  * be notified.
+ * @hide
  */
 public class PackageWatchdog {
     private static final String TAG = "PackageWatchdog";
@@ -108,13 +110,25 @@
     private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10;
 
 
+    /** Reason for package failure could not be determined. */
     public static final int FAILURE_REASON_UNKNOWN = 0;
+
+    /** The package had a native crash. */
     public static final int FAILURE_REASON_NATIVE_CRASH = 1;
+
+    /** The package failed an explicit health check. */
     public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2;
+
+    /** The app crashed. */
     public static final int FAILURE_REASON_APP_CRASH = 3;
+
+    /** The app was not responding. */
     public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4;
+
+    /** The device was boot looping. */
     public static final int FAILURE_REASON_BOOT_LOOP = 5;
 
+    /** @hide */
     @IntDef(prefix = { "FAILURE_REASON_" }, value = {
             FAILURE_REASON_UNKNOWN,
             FAILURE_REASON_NATIVE_CRASH,
@@ -186,7 +200,8 @@
     // aborted.
     private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
 
-    @GuardedBy("PackageWatchdog.class")
+    private static final Object sPackageWatchdogLock = new Object();
+    @GuardedBy("sPackageWatchdogLock")
     private static PackageWatchdog sPackageWatchdog;
 
     private final Object mLock = new Object();
@@ -278,8 +293,8 @@
     }
 
     /** Creates or gets singleton instance of PackageWatchdog. */
-    public static PackageWatchdog getInstance(Context context) {
-        synchronized (PackageWatchdog.class) {
+    public static  @NonNull PackageWatchdog getInstance(@NonNull Context context) {
+        synchronized (sPackageWatchdogLock) {
             if (sPackageWatchdog == null) {
                 new PackageWatchdog(context);
             }
@@ -290,6 +305,7 @@
     /**
      * Called during boot to notify when packages are ready on the device so we can start
      * binding.
+     * @hide
      */
     public void onPackagesReady() {
         synchronized (mLock) {
@@ -311,6 +327,7 @@
      *
      * <p>Observers are expected to call this on boot. It does not specify any packages but
      * it will resume observing any packages requested from a previous boot.
+     * @hide
      */
     public void registerHealthObserver(PackageHealthObserver observer) {
         synchronized (mLock) {
@@ -344,6 +361,7 @@
      *
      * <p>If {@code durationMs} is less than 1, a default monitoring duration
      * {@link #DEFAULT_OBSERVING_DURATION_MS} will be used.
+     * @hide
      */
     public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames,
             long durationMs) {
@@ -407,6 +425,7 @@
      * Unregisters {@code observer} from listening to package failure.
      * Additionally, this stops observing any packages that may have previously been observed
      * even from a previous boot.
+     * @hide
      */
     public void unregisterHealthObserver(PackageHealthObserver observer) {
         mLongTaskHandler.post(() -> {
@@ -425,7 +444,7 @@
      *
      * <p>This method could be called frequently if there is a severe problem on the device.
      */
-    public void onPackageFailure(List<VersionedPackage> packages,
+    public void onPackageFailure(@NonNull List<VersionedPackage> packages,
             @FailureReasons int failureReason) {
         if (packages == null) {
             Slog.w(TAG, "Could not resolve a list of failing packages");
@@ -566,6 +585,7 @@
      *
      * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots
      * are not counted in bootloop.
+     * @hide
      */
     @SuppressWarnings("GuardedBy")
     public void noteBoot() {
@@ -620,7 +640,7 @@
     // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also
     // avoid holding lock?
     // This currently adds about 7ms extra to shutdown thread
-    /** Writes the package information to file during shutdown. */
+    /** @hide Writes the package information to file during shutdown. */
     public void writeNow() {
         synchronized (mLock) {
             // Must only run synchronous tasks as this runs on the ShutdownThread and no other
@@ -674,6 +694,7 @@
      * Since this method can eventually trigger a rollback, it should be called
      * only once boot has completed {@code onBootCompleted} and not earlier, because the install
      * session must be entirely completed before we try to rollback.
+     * @hide
      */
     public void scheduleCheckAndMitigateNativeCrashes() {
         Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check "
@@ -695,7 +716,9 @@
         return mPackagesExemptFromImpactLevelThreshold;
     }
 
-    /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
+    /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}.
+     * @hide
+     */
     @Retention(SOURCE)
     @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
@@ -787,7 +810,7 @@
          * Identifier for the observer, should not change across device updates otherwise the
          * watchdog may drop observing packages with the old name.
          */
-        String getUniqueIdentifier();
+        @NonNull String getUniqueIdentifier();
 
         /**
          * An observer will not be pruned if this is set, even if the observer is not explicitly
@@ -804,7 +827,7 @@
          * <p> A persistent observer may choose to start observing certain failing packages, even if
          * it has not explicitly asked to watch the package with {@link #startObservingHealth}.
          */
-        default boolean mayObservePackage(String packageName) {
+        default boolean mayObservePackage(@NonNull String packageName) {
             return false;
         }
     }
@@ -1240,7 +1263,7 @@
         }
     }
 
-    /** Convert a {@code LongArrayQueue} to a String of comma-separated values. */
+    /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */
     public static String longArrayQueueToString(LongArrayQueue queue) {
         if (queue.size() > 0) {
             StringBuilder sb = new StringBuilder();
@@ -1254,7 +1277,7 @@
         return "";
     }
 
-    /** Parse a comma-separated String of longs into a LongArrayQueue. */
+    /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */
     public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) {
         LongArrayQueue result = new LongArrayQueue();
         if (!TextUtils.isEmpty(commaSeparatedValues)) {
@@ -1268,7 +1291,7 @@
 
 
     /** Dump status of every observer in mAllObservers. */
-    public void dump(PrintWriter pw) {
+    public void dump(@NonNull PrintWriter pw) {
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
         ipw.println("Package Watchdog status");
         ipw.increaseIndent();
@@ -1395,6 +1418,7 @@
         /**
          * Increments failure counts of {@code packageName}.
          * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise
+         * @hide
          */
         @GuardedBy("mLock")
         public boolean onPackageFailureLocked(String packageName) {
@@ -1514,6 +1538,7 @@
         }
     }
 
+    /** @hide */
     @Retention(SOURCE)
     @IntDef(value = {
             HealthCheckState.ACTIVE,
@@ -1603,7 +1628,9 @@
             updateHealthCheckStateLocked();
         }
 
-        /** Writes the salient fields to disk using {@code out}. */
+        /** Writes the salient fields to disk using {@code out}.
+         * @hide
+         */
         @GuardedBy("mLock")
         public void writeLocked(TypedXmlSerializer out) throws IOException {
             out.startTag(null, TAG_PACKAGE);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index a13ce65..bae9a67 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -40,6 +40,7 @@
 import android.aconfigd.Aconfigd.StorageReturnMessage;
 import android.aconfigd.Aconfigd.StorageReturnMessages;
 import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
+import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -491,14 +492,18 @@
     static void writeFlagOverrideRequest(
         ProtoOutputStream proto, String packageName, String flagName, String flagValue,
         boolean isLocal) {
+      int localOverrideTag = supportImmediateLocalOverrides()
+          ? StorageRequestMessage.LOCAL_IMMEDIATE
+          : StorageRequestMessage.LOCAL_ON_REBOOT;
+
       long msgsToken = proto.start(StorageRequestMessages.MSGS);
       long msgToken = proto.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE);
       proto.write(StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, packageName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flagName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flagValue);
       proto.write(StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, isLocal
-                ? StorageRequestMessage.LOCAL_ON_REBOOT
-                : StorageRequestMessage.SERVER_ON_REBOOT);
+            ? localOverrideTag
+            : StorageRequestMessage.SERVER_ON_REBOOT);
       proto.end(msgToken);
       proto.end(msgsToken);
     }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 9b51b6a..4f6da3b 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -205,4 +205,12 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+
+flag {
+    name: "defer_display_events_when_frozen"
+    namespace: "system_performance"
+    is_fixed_read_only: true
+    description: "Defer submitting display events to frozen processes."
+    bug: "326315985"
+}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index feef540..4c91789 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -725,7 +725,7 @@
                 return -1;
             }
 
-            if (!Utils.isValidAuthenticatorConfig(promptInfo)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), promptInfo)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -763,7 +763,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1038,7 +1038,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1060,7 +1060,7 @@
 
             Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 407ef1e..de7bce7 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
@@ -233,17 +234,18 @@
      * @param promptInfo
      * @return
      */
-    static boolean isValidAuthenticatorConfig(PromptInfo promptInfo) {
+    static boolean isValidAuthenticatorConfig(Context context, PromptInfo promptInfo) {
         final int authenticators = promptInfo.getAuthenticators();
-        return isValidAuthenticatorConfig(authenticators);
+        return isValidAuthenticatorConfig(context, authenticators);
     }
 
     /**
-     * Checks if the authenticator configuration is a valid combination of the public APIs
-     * @param authenticators
-     * @return
+     * Checks if the authenticator configuration is a valid combination of the public APIs.
+     *
+     * throws {@link SecurityException} if the caller requests for mandatory biometrics without
+     * {@link SET_BIOMETRIC_DIALOG_ADVANCED} permission
      */
-    static boolean isValidAuthenticatorConfig(int authenticators) {
+    static boolean isValidAuthenticatorConfig(Context context, int authenticators) {
         // The caller is not required to set the authenticators. But if they do, check the below.
         if (authenticators == 0) {
             return true;
@@ -271,6 +273,9 @@
         } else if (biometricBits == Authenticators.BIOMETRIC_WEAK) {
             return true;
         } else if (isMandatoryBiometricsRequested(authenticators)) {
+            //TODO(b/347123256): Update CTS test
+            context.enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_ADVANCED,
+                    "Must have SET_BIOMETRIC_DIALOG_ADVANCED permission");
             return true;
         }
 
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
index 5f2fbce..8a81aaa 100644
--- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
+++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
@@ -23,7 +23,10 @@
 import com.android.server.SystemService;
 
 
-/** This class encapsulate the lifecycle methods of CrashRecovery module. */
+/** This class encapsulate the lifecycle methods of CrashRecovery module.
+ *
+ * @hide
+ */
 public class CrashRecoveryModule {
     private static final String TAG = "CrashRecoveryModule";
 
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 1f9eb08..67e2ca2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -48,6 +48,7 @@
 import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
+import static android.text.TextUtils.formatSimple;
 import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
 
 import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
@@ -279,6 +280,8 @@
     private InputManagerInternal mInputManagerInternal;
     private ActivityManagerInternal mActivityManagerInternal;
     private final UidImportanceListener mUidImportanceListener = new UidImportanceListener();
+    private final DisplayFrozenProcessListener mDisplayFrozenProcessListener;
+
     @Nullable
     private IMediaProjectionManager mProjectionService;
     private DeviceStateManagerInternal mDeviceStateManager;
@@ -321,6 +324,12 @@
     @GuardedBy("mSyncRoot")
     private final SparseArray<CallbackRecord> mCallbacks = new SparseArray<>();
 
+    // All callback records indexed by [uid][pid], for fast lookup by uid.
+    // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+    @GuardedBy("mSyncRoot")
+    private final SparseArray<SparseArray<CallbackRecord>> mCallbackRecordByPidByUid =
+            new SparseArray<>();
+
     /**
      *  All {@link IVirtualDevice} and {@link DisplayWindowPolicyController}s indexed by
      *  {@link DisplayInfo#displayId}.
@@ -472,6 +481,7 @@
 
     // Pending callback records indexed by calling process uid and pid.
     // Must be used outside of the lock mSyncRoot and should be self-locked.
+    // This is only used when {@link deferDisplayEventsWhenFrozen()} is false.
     @GuardedBy("mPendingCallbackSelfLocked")
     private final SparseArray<SparseArray<PendingCallback>> mPendingCallbackSelfLocked =
             new SparseArray<>();
@@ -611,6 +621,7 @@
         mFlags = injector.getFlags();
         mHandler = new DisplayManagerHandler(displayThreadLooper);
         mHandlerExecutor = new HandlerExecutor(mHandler);
+        mDisplayFrozenProcessListener = new DisplayFrozenProcessListener();
         mUiHandler = UiThread.getHandler();
         mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore);
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContext,
@@ -1034,10 +1045,14 @@
     private class UidImportanceListener implements ActivityManager.OnUidImportanceListener {
         @Override
         public void onUidImportance(int uid, int importance) {
-          onUidImportanceInternal(uid, importance);
+            if (deferDisplayEventsWhenFrozen()) {
+                onUidImportanceFlagged(uid, importance);
+            } else {
+                onUidImportanceUnflagged(uid, importance);
+            }
         }
 
-        private void onUidImportanceInternal(int uid, int importance) {
+        private void onUidImportanceUnflagged(int uid, int importance) {
             synchronized (mPendingCallbackSelfLocked) {
                 if (importance >= IMPORTANCE_GONE) {
                     // Clean up as the app is already gone
@@ -1068,6 +1083,83 @@
                 mPendingCallbackSelfLocked.delete(uid);
             }
         }
+
+        private void onUidImportanceFlagged(int uid, int importance) {
+            final boolean cached = (importance >= IMPORTANCE_CACHED);
+            List<CallbackRecord> readyCallbackRecords = null;
+            synchronized (mSyncRoot) {
+                final SparseArray<CallbackRecord> procs = mCallbackRecordByPidByUid.get(uid);
+                if (procs == null) {
+                    return;
+                }
+                if (cached) {
+                    setCachedLocked(procs);
+                } else {
+                    readyCallbackRecords = setUncachedLocked(procs);
+                }
+            }
+            if (readyCallbackRecords != null) {
+                // Attempt to dispatch pending events if the UID is coming out of cached state.
+                for (int i = 0; i < readyCallbackRecords.size(); i++) {
+                    readyCallbackRecords.get(i).dispatchPending();
+                }
+            }
+        }
+
+        // Set all processes in the list to cached.
+        @GuardedBy("mSyncRoot")
+        private void setCachedLocked(SparseArray<CallbackRecord> procs) {
+            for (int i = 0; i < procs.size(); i++) {
+                final CallbackRecord cb = procs.valueAt(i);
+                if (cb != null) {
+                    cb.setCached(true);
+                }
+            }
+        }
+
+        // Set all processes to uncached and return the list of processes that were modified.
+        @GuardedBy("mSyncRoot")
+        private List<CallbackRecord> setUncachedLocked(SparseArray<CallbackRecord> procs) {
+            ArrayList<CallbackRecord> ready = null;
+            for (int i = 0; i < procs.size(); i++) {
+                final CallbackRecord cb = procs.valueAt(i);
+                if (cb != null) {
+                    if (cb.setCached(false)) {
+                        if (ready == null) ready = new ArrayList<>();
+                        ready.add(cb);
+                    }
+                }
+            }
+            return ready;
+        }
+    }
+
+    private class DisplayFrozenProcessListener
+            implements ActivityManagerInternal.FrozenProcessListener {
+        public void onProcessFrozen(int pid) {
+            synchronized (mSyncRoot) {
+                CallbackRecord callback = mCallbacks.get(pid);
+                if (callback == null) {
+                    return;
+                }
+                callback.setFrozen(true);
+            }
+        }
+
+        public void onProcessUnfrozen(int pid) {
+            // First, see if there is a callback associated with this pid.  If there's no
+            // callback, then there is nothing to do.
+            CallbackRecord callback;
+            synchronized (mSyncRoot) {
+                callback = mCallbacks.get(pid);
+                if (callback == null) {
+                    return;
+                }
+                callback.setFrozen(false);
+            }
+            // Attempt to dispatch pending events if the process is coming out of frozen.
+            callback.dispatchPending();
+        }
     }
 
     private class SettingsObserver extends ContentObserver {
@@ -1314,12 +1406,29 @@
             }
 
             mCallbacks.put(callingPid, record);
+            if (deferDisplayEventsWhenFrozen()) {
+                SparseArray<CallbackRecord> uidPeers = mCallbackRecordByPidByUid.get(record.mUid);
+                if (uidPeers == null) {
+                    uidPeers = new SparseArray<CallbackRecord>();
+                    mCallbackRecordByPidByUid.put(record.mUid, uidPeers);
+                }
+                uidPeers.put(record.mPid, record);
+            }
         }
     }
 
     private void onCallbackDied(CallbackRecord record) {
         synchronized (mSyncRoot) {
             mCallbacks.remove(record.mPid);
+            if (deferDisplayEventsWhenFrozen()) {
+                SparseArray<CallbackRecord> uidPeers = mCallbackRecordByPidByUid.get(record.mUid);
+                if (uidPeers != null) {
+                    uidPeers.remove(record.mPid);
+                    if (uidPeers.size() == 0) {
+                        mCallbackRecordByPidByUid.remove(record.mUid);
+                    }
+                }
+            }
             stopWifiDisplayScanLocked(record);
         }
     }
@@ -3296,12 +3405,16 @@
         // After releasing the lock, send the notifications out.
         for (int i = 0; i < mTempCallbacks.size(); i++) {
             CallbackRecord callbackRecord = mTempCallbacks.get(i);
-            deliverEventInternal(callbackRecord, displayId, event);
+            if (deferDisplayEventsWhenFrozen()) {
+                deliverEventFlagged(callbackRecord, displayId, event);
+            } else {
+                deliverEventUnflagged(callbackRecord, displayId, event);
+            }
         }
         mTempCallbacks.clear();
     }
 
-    private void deliverEventInternal(CallbackRecord callbackRecord, int displayId, int event) {
+    private void deliverEventUnflagged(CallbackRecord callbackRecord, int displayId, int event) {
         final int uid = callbackRecord.mUid;
         final int pid = callbackRecord.mPid;
         if (isUidCached(uid)) {
@@ -3330,6 +3443,10 @@
         }
     }
 
+    private void deliverEventFlagged(CallbackRecord callbackRecord, int displayId, int event) {
+        callbackRecord.notifyDisplayEventAsync(displayId, event);
+    }
+
     private boolean extraLogging(String packageName) {
         return mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals(packageName);
     }
@@ -3454,9 +3571,7 @@
             pw.println("Callbacks: size=" + callbackCount);
             pw.println("-----------------");
             for (int i = 0; i < callbackCount; i++) {
-                CallbackRecord callback = mCallbacks.valueAt(i);
-                pw.println("  " + i + ": mPid=" + callback.mPid
-                        + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested);
+                pw.println("  " + i + ": " + mCallbacks.valueAt(i).dump());
             }
 
             pw.println();
@@ -3855,12 +3970,43 @@
 
         public boolean mWifiDisplayScanRequested;
 
+        // A single pending event.
+        private record Event(int displayId, @DisplayEvent int event) { };
+
+        // The list of pending events.  This is null until there is a pending event to be saved.
+        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+        @GuardedBy("mCallback")
+        private ArrayList<Event> mPendingEvents;
+
+        // Process states: a process is ready to receive events if it is neither cached nor
+        // frozen.
+        @GuardedBy("mCallback")
+        private boolean mCached;
+        @GuardedBy("mCallback")
+        private boolean mFrozen;
+
         CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback,
                 @EventsMask long eventsMask) {
             mPid = pid;
             mUid = uid;
             mCallback = callback;
             mEventsMask = new AtomicLong(eventsMask);
+            mCached = false;
+            mFrozen = false;
+
+            if (deferDisplayEventsWhenFrozen()) {
+                // Some CallbackRecords are registered very early in system boot, before
+                // mActivityManagerInternal is initialized. If mActivityManagerInternal is null,
+                // do not register the frozen process listener.  However, do verify that all such
+                // registrations are for the self pid (which can never be frozen, so the frozen
+                // process listener does not matter).
+                if (mActivityManagerInternal != null) {
+                    mActivityManagerInternal.addFrozenProcessListener(pid, mHandlerExecutor,
+                            mDisplayFrozenProcessListener);
+                } else if (Process.myPid() != pid) {
+                    Slog.e(TAG, "DisplayListener registered too early");
+                }
+            }
 
             String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
             mPackageName = packageNames == null ? null : packageNames[0];
@@ -3870,6 +4016,46 @@
             mEventsMask.set(eventsMask);
         }
 
+        /**
+         * Return true if the process can accept events.
+         */
+        @GuardedBy("mCallback")
+        private boolean isReadyLocked() {
+            return !mCached && !mFrozen;
+        }
+
+        /**
+         * Return true if the process is now ready and has pending events to be delivered.
+         */
+        @GuardedBy("mCallback")
+        private boolean hasPendingAndIsReadyLocked() {
+            return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty();
+        }
+
+        /**
+         * Set the frozen flag for this process.  Return true if the process is now ready to
+         * receive events and there are pending events to be delivered.
+         * This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+         */
+        public boolean setFrozen(boolean frozen) {
+            synchronized (mCallback) {
+                mFrozen = frozen;
+                return hasPendingAndIsReadyLocked();
+            }
+        }
+
+        /**
+         * Set the cached flag for this process.  Return true if the process is now ready to
+         * receive events and there are pending events to be delivered.
+         * This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+         */
+        public boolean setCached(boolean cached) {
+            synchronized (mCallback) {
+                mCached = cached;
+                return hasPendingAndIsReadyLocked();
+            }
+        }
+
         @Override
         public void binderDied() {
             if (DEBUG || extraLogging(mPackageName)) {
@@ -3885,7 +4071,7 @@
         /**
          * @return {@code false} if RemoteException happens; otherwise {@code true} for
          * success.  This returns true even if the event was deferred because the remote client is
-         * cached.
+         * cached or frozen.
          */
         public boolean notifyDisplayEventAsync(int displayId, @DisplayEvent int event) {
             if (!shouldSendEvent(event)) {
@@ -3902,6 +4088,22 @@
                 return true;
             }
 
+            if (deferDisplayEventsWhenFrozen()) {
+                synchronized (mCallback) {
+                    // Add the new event to the pending list if the client frozen or cached (not
+                    // ready) or if there are existing pending events.  The latter condition
+                    // occurs as the client is transitioning to ready but pending events have not
+                    // been dispatched.  The new event must be added to the pending list to
+                    // preserve event ordering.
+                    if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) {
+                        // The client is interested in the event but is not ready to receive it.
+                        // Put the event on the pending list.
+                        addDisplayEvent(displayId, event);
+                        return true;
+                    }
+                }
+            }
+
             return transmitDisplayEvent(displayId, event);
         }
 
@@ -3948,8 +4150,81 @@
                     return true;
             }
         }
+
+        // Add a single event to the pending list, possibly combining or collapsing events in the
+        // list.
+        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+        @GuardedBy("mCallback")
+        private void addDisplayEvent(int displayId, int event) {
+            if (mPendingEvents == null) {
+                mPendingEvents = new ArrayList<>();
+            }
+            if (!mPendingEvents.isEmpty()) {
+                // Ignore redundant events. Further optimization is possible by merging adjacent
+                // events.
+                Event last = mPendingEvents.get(mPendingEvents.size() - 1);
+                if (last.displayId == displayId && last.event == event) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event
+                                + " to " + mUid + "/" + mPid);
+                    }
+                    return;
+                }
+            }
+            mPendingEvents.add(new Event(displayId, event));
+        }
+
+        // Send all pending events.  This can safely be called if the process is not ready, but it
+        // would be unusual to do so.  The method returns true on success.
+        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+        public boolean dispatchPending() {
+            synchronized (mCallback) {
+                if (mPendingEvents == null || mPendingEvents.isEmpty()) {
+                    return true;
+                }
+                if (!isReadyLocked()) {
+                    return false;
+                }
+                for (int i = 0; i < mPendingEvents.size(); i++) {
+                    Event displayEvent = mPendingEvents.get(i);
+                    if (DEBUG) {
+                        Slog.d(TAG, "Send pending display event #" + i + " "
+                                + displayEvent.displayId + "/"
+                                + displayEvent.event + " to " + mUid + "/" + mPid);
+                    }
+                    if (!transmitDisplayEvent(displayEvent.displayId, displayEvent.event)) {
+                        Slog.d(TAG, "Drop pending events for dead process " + mPid);
+                        break;
+                    }
+                }
+                mPendingEvents.clear();
+                return true;
+            }
+        }
+
+        // Return a string suitable for dumpsys.
+        private String dump() {
+            if (deferDisplayEventsWhenFrozen()) {
+                final String fmt =
+                        "mPid=%d mUid=%d mWifiDisplayScanRequested=%s"
+                        + " cached=%s frozen=%s pending=%d";
+                synchronized (mCallback) {
+                    return formatSimple(fmt,
+                            mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen,
+                            (mPendingEvents == null) ? 0 : mPendingEvents.size());
+                }
+            } else {
+                final String fmt =
+                        "mPid=%d mUid=%d mWifiDisplayScanRequested=%s";
+                return formatSimple(fmt,
+                        mPid, mUid, mWifiDisplayScanRequested);
+            }
+        }
     }
 
+    /**
+     * This is only used if {@link deferDisplayEventsWhenFrozen()} is false.
+     */
     private static final class PendingCallback {
         private final CallbackRecord mCallbackRecord;
         private final ArrayList<Pair<Integer, Integer>> mDisplayEvents;
@@ -5504,4 +5779,11 @@
             return mExternalDisplayStatsService;
         }
     }
+
+    /**
+     * Return the value of the pause
+     */
+    private static boolean deferDisplayEventsWhenFrozen() {
+        return com.android.server.am.Flags.deferDisplayEventsWhenFrozen();
+    }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 62d8761..03fec011 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1480,29 +1480,24 @@
             brightnessState = clampScreenBrightness(brightnessState);
         }
 
-        if (useDozeBrightness) {
-            // TODO(b/329676661): Introduce a config property to choose between this brightness
-            //  strategy and DOZE_DEFAULT
-            // On some devices, when auto-brightness is disabled and the device is dozing, we use
-            // the current brightness setting scaled by the doze scale factor
-            if ((Float.isNaN(brightnessState)
-                    || displayBrightnessState.getDisplayBrightnessStrategyName()
-                    .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))
-                    && mFlags.isDisplayOffloadEnabled()
-                    && mDisplayOffloadSession != null
+        if (useDozeBrightness && (Float.isNaN(brightnessState)
+                || displayBrightnessState.getDisplayBrightnessStrategyName()
+                .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))) {
+            if (mFlags.isDisplayOffloadEnabled() && mDisplayOffloadSession != null
                     && (mAutomaticBrightnessController == null
                     || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness())) {
+                // TODO(b/329676661): Introduce a config property to choose between this brightness
+                //  strategy and DOZE_DEFAULT
+                // On some devices, when auto-brightness is disabled and the device is dozing, we
+                // use the current brightness setting scaled by the doze scale factor
                 rawBrightnessState = getDozeBrightnessForOffload();
                 brightnessState = clampScreenBrightness(rawBrightnessState);
                 updateScreenBrightnessSetting = false;
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL);
                 mTempBrightnessEvent.setFlags(
                         mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE);
-            }
-
-            // Use default brightness when dozing unless overridden.
-            if (Float.isNaN(brightnessState)
-                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) {
+            } else if (!mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) {
+                // Use default brightness when dozing unless overridden.
                 rawBrightnessState = mScreenBrightnessDozeConfig;
                 brightnessState = clampScreenBrightness(rawBrightnessState);
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index abb2132..06f419a 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -118,6 +118,37 @@
             Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false)
     );
 
+    // Bits 1, 2, 3, 4 are already taken by: beep|buzz|blink|cooldown
+    static final int MUTE_REASON_NOT_MUTED = 0;
+    static final int MUTE_REASON_NOT_AUDIBLE = 1 << 5;
+    static final int MUTE_REASON_SILENT_UPDATE = 1 << 6;
+    static final int MUTE_REASON_POST_SILENTLY = 1 << 7;
+    static final int MUTE_REASON_LISTENER_HINT = 1 << 8;
+    static final int MUTE_REASON_DND = 1 << 9;
+    static final int MUTE_REASON_GROUP_ALERT = 1 << 10;
+    static final int MUTE_REASON_FLAG_SILENT = 1 << 11;
+    static final int MUTE_REASON_RATE_LIMIT = 1 << 12;
+    static final int MUTE_REASON_OTHER_INSISTENT_PLAYING = 1 << 13;
+    static final int MUTE_REASON_SUPPRESSED_BUBBLE = 1 << 14;
+    static final int MUTE_REASON_COOLDOWN = 1 << 15;
+
+    @IntDef(prefix = { "MUTE_REASON_" }, value = {
+        MUTE_REASON_NOT_MUTED,
+        MUTE_REASON_NOT_AUDIBLE,
+        MUTE_REASON_SILENT_UPDATE,
+        MUTE_REASON_POST_SILENTLY,
+        MUTE_REASON_LISTENER_HINT,
+        MUTE_REASON_DND,
+        MUTE_REASON_GROUP_ALERT,
+        MUTE_REASON_FLAG_SILENT,
+        MUTE_REASON_RATE_LIMIT,
+        MUTE_REASON_OTHER_INSISTENT_PLAYING,
+        MUTE_REASON_SUPPRESSED_BUBBLE,
+        MUTE_REASON_COOLDOWN,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface MuteReason {}
+
     private final Context mContext;
     private final PackageManager mPackageManager;
     private final TelephonyManager mTelephonyManager;
@@ -388,6 +419,7 @@
         boolean buzz = false;
         boolean beep = false;
         boolean blink = false;
+        @MuteReason int shouldMuteReason = MUTE_REASON_NOT_MUTED;
 
         final String key = record.getKey();
 
@@ -395,10 +427,6 @@
             Log.d(TAG, "buzzBeepBlinkLocked " + record);
         }
 
-        if (isPoliteNotificationFeatureEnabled(record)) {
-            mStrategy.onNotificationPosted(record);
-        }
-
         // Should this notification make noise, vibe, or use the LED?
         final boolean aboveThreshold =
                 mIsAutomotive
@@ -443,7 +471,8 @@
                 boolean vibrateOnly =
                         hasValidVibrate && mNotificationCooldownVibrateUnlocked && mUserPresent;
                 boolean hasAudibleAlert = hasValidSound || hasValidVibrate;
-                if (hasAudibleAlert && !shouldMuteNotificationLocked(record, signals)) {
+                shouldMuteReason = shouldMuteNotificationLocked(record, signals, hasAudibleAlert);
+                if (shouldMuteReason == MUTE_REASON_NOT_MUTED) {
                     if (!sentAccessibilityEvent) {
                         sendAccessibilityEvent(record);
                         sentAccessibilityEvent = true;
@@ -541,15 +570,17 @@
             }
         }
         final int buzzBeepBlinkLoggingCode =
-                (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) | getPoliteBit(record);
+                (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)
+                | getPoliteBit(record) | shouldMuteReason;
         if (buzzBeepBlinkLoggingCode > 0) {
             MetricsLogger.action(record.getLogMaker()
                     .setCategory(MetricsEvent.NOTIFICATION_ALERT)
                     .setType(MetricsEvent.TYPE_OPEN)
                     .setSubtype(buzzBeepBlinkLoggingCode));
             EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0,
-                    getPolitenessState(record));
+                    getPolitenessState(record), shouldMuteReason);
         }
+
         if (Flags.politeNotifications()) {
             // Update last alert time
             if (buzz || beep) {
@@ -594,41 +625,46 @@
                 mNMP.getNotificationByKey(mVibrateNotificationKey));
     }
 
-    boolean shouldMuteNotificationLocked(final NotificationRecord record, final Signals signals) {
+    @MuteReason int shouldMuteNotificationLocked(final NotificationRecord record,
+            final Signals signals, boolean hasAudibleAlert) {
+        // Suppressed because no audible alert
+        if (!hasAudibleAlert) {
+            return MUTE_REASON_NOT_AUDIBLE;
+        }
         // Suppressed because it's a silent update
         final Notification notification = record.getNotification();
         if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) {
-            return true;
+            return MUTE_REASON_SILENT_UPDATE;
         }
 
         // Suppressed because a user manually unsnoozed something (or similar)
         if (record.shouldPostSilently()) {
-            return true;
+            return MUTE_REASON_POST_SILENTLY;
         }
 
         // muted by listener
         final String disableEffects = disableNotificationEffects(record, signals.listenerHints);
         if (disableEffects != null) {
             ZenLog.traceDisableEffects(record, disableEffects);
-            return true;
+            return MUTE_REASON_LISTENER_HINT;
         }
 
         // suppressed due to DND
         if (record.isIntercepted()) {
-            return true;
+            return MUTE_REASON_DND;
         }
 
         // Suppressed because another notification in its group handles alerting
         if (record.getSbn().isGroup()) {
             if (notification.suppressAlertingDueToGrouping()) {
-                return true;
+                return MUTE_REASON_GROUP_ALERT;
             }
         }
 
         // Suppressed because notification was explicitly flagged as silent
         if (android.service.notification.Flags.notificationSilentFlag()) {
             if (notification.isSilent()) {
-                return true;
+                return MUTE_REASON_FLAG_SILENT;
             }
         }
 
@@ -636,12 +672,12 @@
         final String pkg = record.getSbn().getPackageName();
         if (mUsageStats.isAlertRateLimited(pkg)) {
             Slog.e(TAG, "Muting recently noisy " + record.getKey());
-            return true;
+            return MUTE_REASON_RATE_LIMIT;
         }
 
         // A different looping ringtone, such as an incoming call is playing
         if (isCurrentlyInsistent() && !isInsistentUpdate(record)) {
-            return true;
+            return MUTE_REASON_OTHER_INSISTENT_PLAYING;
         }
 
         // Suppressed since it's a non-interruptive update to a bubble-suppressed notification
@@ -650,11 +686,23 @@
         if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed
                 && record.getNotification().getBubbleMetadata() != null) {
             if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) {
-                return true;
+                return MUTE_REASON_SUPPRESSED_BUBBLE;
             }
         }
 
-        return false;
+        if (isPoliteNotificationFeatureEnabled(record)) {
+            // Notify the politeness strategy that an alerting notification is posted
+            if (!isInsistentUpdate(record)) {
+                mStrategy.onNotificationPosted(record);
+            }
+
+            // Suppress if politeness is muted and it's not an update for insistent
+            if (getPolitenessState(record) == PolitenessStrategy.POLITE_STATE_MUTED) {
+                return MUTE_REASON_COOLDOWN;
+            }
+        }
+
+        return MUTE_REASON_NOT_MUTED;
     }
 
     private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) {
@@ -1201,12 +1249,6 @@
             mApplyPerPackage = applyPerPackage;
         }
 
-        boolean shouldIgnoreNotification(final NotificationRecord record) {
-            // Ignore auto-group summaries => don't count them as app-posted notifications
-            // for the cooldown budget
-            return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record));
-        }
-
         /**
          * Get the key that determines the grouping for the cooldown behavior.
          *
@@ -1358,10 +1400,6 @@
 
         @Override
         public void onNotificationPosted(final NotificationRecord record) {
-            if (shouldIgnoreNotification(record)) {
-                return;
-            }
-
             long timeSinceLastNotif =
                     System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record);
 
@@ -1434,10 +1472,6 @@
         @Override
         void onNotificationPosted(NotificationRecord record) {
             if (isAvalancheActive()) {
-                if (shouldIgnoreNotification(record)) {
-                    return;
-                }
-
                 long timeSinceLastNotif =
                     System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record);
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 79633f1..655f2e4 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -6248,6 +6248,7 @@
             int callingUid = Binder.getCallingUid();
             @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser);
 
+            boolean isSystemCaller = isCallerSystemOrSystemUiOrShell();
             boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
                     && !canManageGlobalZenPolicy(pkg, callingUid);
 
@@ -6284,11 +6285,33 @@
                             policy.priorityCallSenders, policy.priorityMessageSenders,
                             policy.suppressedVisualEffects, currPolicy.priorityConversationSenders);
                 }
+
                 int newVisualEffects = calculateSuppressedVisualEffects(
                             policy, currPolicy, applicationInfo.targetSdkVersion);
-                policy = new Policy(policy.priorityCategories,
-                        policy.priorityCallSenders, policy.priorityMessageSenders,
-                        newVisualEffects, policy.priorityConversationSenders);
+
+                if (android.app.Flags.modesUi()) {
+                    // 1. Callers should not modify STATE_CHANNELS_BYPASSING_DND, which is
+                    // internally calculated and only indicates whether channels that want to bypass
+                    // DND _exist_.
+                    // 2. Only system callers should modify STATE_PRIORITY_CHANNELS_BLOCKED because
+                    // it is @hide.
+                    // 3. If the policy has been modified by the targetSdkVersion checks above then
+                    // it has lost its state flags and that's fine (STATE_PRIORITY_CHANNELS_BLOCKED
+                    // didn't exist until V).
+                    int newState = Policy.STATE_UNSET;
+                    if (isSystemCaller && policy.state != Policy.STATE_UNSET) {
+                        newState = Policy.policyState(
+                                currPolicy.hasPriorityChannels(),
+                                policy.allowPriorityChannels());
+                    }
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, newState, policy.priorityConversationSenders);
+                } else {
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, policy.priorityConversationSenders);
+                }
 
                 if (shouldApplyAsImplicitRule) {
                     mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index f78c448..d206c66 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -99,6 +99,7 @@
     // True if needing to roll back only rebootless apexes when native crash happens
     private boolean mTwoPhaseRollbackEnabled;
 
+    /** @hide */
     @VisibleForTesting
     public RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
         mContext = context;
@@ -123,7 +124,7 @@
         }
     }
 
-    RollbackPackageHealthObserver(Context context) {
+    public RollbackPackageHealthObserver(@NonNull Context context) {
         this(context, ApexManager.getInstance());
     }
 
@@ -239,8 +240,8 @@
         return false;
     }
 
-
     @Override
+    @NonNull
     public String getUniqueIdentifier() {
         return NAME;
     }
@@ -251,7 +252,7 @@
     }
 
     @Override
-    public boolean mayObservePackage(String packageName) {
+    public boolean mayObservePackage(@NonNull String packageName) {
         if (getAvailableRollbacks().isEmpty()) {
             return false;
         }
@@ -281,12 +282,14 @@
      * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
      */
     @AnyThread
-    void startObservingHealth(List<String> packages, long durationMs) {
+    @NonNull
+    public void startObservingHealth(@NonNull List<String> packages, @NonNull long durationMs) {
         PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
     }
 
     @AnyThread
-    void notifyRollbackAvailable(RollbackInfo rollback) {
+    @NonNull
+    public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) {
         mHandler.post(() -> {
             // Enable two-phase rollback when a rebootless apex rollback is made available.
             // We assume the rebootless apex is stable and is less likely to be the cause
@@ -314,7 +317,7 @@
      * to check for native crashes and mitigate them if needed.
      */
     @AnyThread
-    void onBootCompletedAsync() {
+    public void onBootCompletedAsync() {
         mHandler.post(()->onBootCompleted());
     }
 
diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
index 79560ce..9cfed02 100644
--- a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
+++ b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
@@ -51,6 +51,7 @@
 
 /**
  * This class handles the logic for logging Watchdog-triggered rollback events.
+ * @hide
  */
 public final class WatchdogRollbackLogger {
     private static final String TAG = "WatchdogRollbackLogger";
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 4754ffb..2d75f35 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -20,6 +20,7 @@
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.Manifest.permission.READ_WALLPAPER_INTERNAL;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+import static android.app.Flags.fixWallpaperChanged;
 import static android.app.Flags.removeNextWallpaperComponent;
 import static android.app.WallpaperManager.COMMAND_REAPPLY;
 import static android.app.WallpaperManager.FLAG_LOCK;
@@ -349,7 +350,10 @@
                             if (DEBUG) {
                                 Slog.d(TAG, "publish system wallpaper changed!");
                             }
-                            notifyWallpaperChanged(wallpaper);
+                            notifyWallpaperComplete(wallpaper);
+                            if (fixWallpaperChanged()) {
+                                notifyWallpaperChanged(wallpaper);
+                            }
                         }
                     };
 
@@ -369,7 +373,10 @@
                             if (DEBUG) {
                                 Slog.d(TAG, "publish lock wallpaper changed!");
                             }
-                            notifyWallpaperChanged(wallpaper);
+                            notifyWallpaperComplete(wallpaper);
+                            if (fixWallpaperChanged()) {
+                                notifyWallpaperChanged(wallpaper);
+                            }
                         }
                     };
 
@@ -403,8 +410,11 @@
         }
     }
 
-    private void notifyWallpaperChanged(WallpaperData wallpaper) {
-        // Publish completion *after* we've persisted the changes
+    /*
+     * Calls wallpaper setComplete methods. Called for static wallpapers after the wallpaper is set
+     * and changes are persisted.
+     */
+    private void notifyWallpaperComplete(WallpaperData wallpaper) {
         if (wallpaper.setComplete != null) {
             try {
                 wallpaper.setComplete.onWallpaperChanged();
@@ -1468,7 +1478,7 @@
                         || change == PACKAGE_TEMPORARY_CHANGE) {
                     changed = true;
                     if (doit) {
-                        Slog.w(TAG, "Wallpaper uninstalled, removing: "
+                        Slog.e(TAG, "Wallpaper uninstalled, removing: "
                                 + wallpaper.getComponent());
                         clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                     }
@@ -1491,7 +1501,7 @@
                             PackageManager.MATCH_DIRECT_BOOT_AWARE
                                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
                 } catch (NameNotFoundException e) {
-                    Slog.w(TAG, "Wallpaper component gone, removing: "
+                    Slog.e(TAG, "Wallpaper component gone, removing: "
                             + wallpaper.getComponent());
                     clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                 }
@@ -1787,6 +1797,9 @@
                     switchWallpaper(systemWallpaper, null);
                     // TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper
                     notifyCallbacksLocked(systemWallpaper);
+                    if (fixWallpaperChanged()) {
+                        notifyWallpaperChanged(systemWallpaper);
+                    }
                 }
                 if (mLockWallpaperWaitingForUnlock) {
                     final WallpaperData lockWallpaper =
@@ -1794,6 +1807,9 @@
                     lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
                     switchWallpaper(lockWallpaper, null);
                     notifyCallbacksLocked(lockWallpaper);
+                    if (fixWallpaperChanged()) {
+                        notifyWallpaperChanged(lockWallpaper);
+                    }
                 }
 
                 // Make sure that the SELinux labeling of all the relevant files is correct.
@@ -3248,6 +3264,9 @@
                     }
                     newWallpaper.wallpaperId = makeWallpaperIdLocked();
                     notifyCallbacksLocked(newWallpaper);
+                    if (fixWallpaperChanged()) {
+                        notifyWallpaperChanged(newWallpaper);
+                    }
                     shouldNotifyColors = true;
                     if (offloadColorExtraction()) {
                         shouldNotifyColors = false;
@@ -3316,6 +3335,21 @@
         return false;
     }
 
+    /*
+     * Attempt to bind the wallpaper given by `componentName`, returning true on success otherwise
+     * false.
+     *
+     * When called, `wallpaper` is in a deliberately inconsistent state. Most fields have been
+     * updated to describe the desired wallpaper, but the ComponentName is not updated until
+     * binding is successful. This is required for maybeDetachWallpapers() to work correctly.
+     *
+     * The late update of the component field should cause multi-threading headaches with
+     * WallpaperConnection#onServiceConnected, but doesn't because onServiceConnected required
+     * `mLock` and `bindWallpaperComponentLocked` is always called with that lock, which prevents a
+     * race condition.
+     *
+     * This is a major motivation for making WallpaperData immutable per b/267170056.
+     */
     boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force,
             boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {
         if (DEBUG_LIVE) {
@@ -3610,8 +3644,18 @@
         }
         wallpaper.callbacks.finishBroadcast();
 
+        if (!fixWallpaperChanged()) {
+            final Intent intent = new Intent(Intent.ACTION_WALLPAPER_CHANGED);
+            intent.putExtra(WallpaperManager.EXTRA_FROM_FOREGROUND_APP,
+                    wallpaper.fromForegroundApp);
+            mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId));
+        }
+    }
+
+    private void notifyWallpaperChanged(WallpaperData wallpaper) {
         final Intent intent = new Intent(Intent.ACTION_WALLPAPER_CHANGED);
         intent.putExtra(WallpaperManager.EXTRA_FROM_FOREGROUND_APP, wallpaper.fromForegroundApp);
+        intent.putExtra(WallpaperManager.EXTRA_WHICH_WALLPAPER_CHANGED, wallpaper.mWhich);
         mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId));
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 12d733f..ca93075 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2154,7 +2154,10 @@
         }
         mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName);
 
-        mActivityRecordInputSink = new ActivityRecordInputSink(this, sourceRecord);
+        final boolean appOptInTouchPassThrough =
+                options != null && options.isAllowPassThroughOnTouchOutside();
+        mActivityRecordInputSink = new ActivityRecordInputSink(
+                this, sourceRecord, appOptInTouchPassThrough);
 
         mAppActivityEmbeddingSplitsEnabled = isAppActivityEmbeddingSplitsEnabled();
         mAllowUntrustedEmbeddingStateSharing = getAllowUntrustedEmbeddingStateSharingProperty();
@@ -3171,14 +3174,23 @@
         return getWindowConfiguration().canReceiveKeys() && !mWaitForEnteringPinnedMode;
     }
 
-    boolean isResizeable() {
-        return isResizeable(/* checkPictureInPictureSupport */ true);
+    /**
+     * Returns {@code true} if the fixed orientation, aspect ratio, resizability of this activity
+     * will be ignored.
+     */
+    boolean isUniversalResizeable() {
+        return mWmService.mConstants.mIgnoreActivityOrientationRequest
+                && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME
+                // If the user preference respects aspect ratio, then it becomes non-resizable.
+                && !mAppCompatController.getAppCompatOverrides().getAppCompatAspectRatioOverrides()
+                        .shouldApplyUserMinAspectRatioOverride();
     }
 
-    boolean isResizeable(boolean checkPictureInPictureSupport) {
+    boolean isResizeable() {
         return mAtmService.mForceResizableActivities
                 || ActivityInfo.isResizeableMode(info.resizeMode)
-                || (info.supportsPictureInPicture() && checkPictureInPictureSupport)
+                || info.supportsPictureInPicture()
+                || isUniversalResizeable()
                 // If the activity can be embedded, it should inherit the bounds of task fragment.
                 || isEmbedded();
     }
@@ -6397,7 +6409,11 @@
             // and the token could be null.
             return;
         }
-        r.mDisplayContent.mAppCompatCameraPolicy.onActivityRefreshed(r);
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+                .getAppCompatCameraPolicy(r);
+        if (cameraPolicy != null) {
+            cameraPolicy.onActivityRefreshed(r);
+        }
     }
 
     static void splashScreenAttachedLocked(IBinder token) {
@@ -8162,11 +8178,8 @@
     @Override
     @ActivityInfo.ScreenOrientation
     protected int getOverrideOrientation() {
-        final int candidateOrientation;
-        if (!mWmService.mConstants.mIgnoreActivityOrientationRequest
-                || info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) {
-            candidateOrientation = super.getOverrideOrientation();
-        } else {
+        int candidateOrientation = super.getOverrideOrientation();
+        if (isUniversalResizeable() && ActivityInfo.isFixedOrientation(candidateOrientation)) {
             candidateOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         }
         return mAppCompatController.getOrientationPolicy()
@@ -9442,8 +9455,12 @@
         if (!shouldBeResumed(/* activeActivity */ null)) {
             return;
         }
-        mDisplayContent.mAppCompatCameraPolicy.onActivityConfigurationChanging(
-                this, newConfig, lastReportedConfig);
+
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+                this);
+        if (cameraPolicy != null) {
+            cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
+        }
     }
 
     /** Get process configuration, or global config if the process is not set. */
@@ -10025,7 +10042,7 @@
         }
         StringBuilder sb = new StringBuilder(128);
         sb.append("ActivityRecord{");
-        sb.append(Integer.toHexString(System.identityHashCode(this)));
+        sb.append(System.identityHashCode(this));
         sb.append(" u");
         sb.append(mUserId);
         sb.append(' ');
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index 1a19787..fa5beca 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -16,13 +16,18 @@
 
 package com.android.server.wm;
 
+import android.app.ActivityOptions;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.os.Build;
 import android.os.InputConfig;
 import android.view.InputWindowHandle;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 
+import com.android.window.flags.Flags;
+
 /**
  * Creates a InputWindowHandle that catches all touches that would otherwise pass through an
  * Activity.
@@ -35,6 +40,21 @@
     @ChangeId
     static final long ENABLE_TOUCH_OPAQUE_ACTIVITIES = 194480991L;
 
+    // TODO(b/369605358) Update EnabledSince when SDK 36 version code is available.
+    /**
+     * If the app's target SDK is 36+, pass-through touches from a cross-uid overlaying activity is
+     * blocked by default. The activity may opt in to receive pass-through touches using
+     * {@link ActivityOptions#setAllowPassThroughOnTouchOutside}, which allows the to-be-launched
+     * cross-uid overlaying activity and other activities in that app to pass through touches. The
+     * activity needs to ensure that it trusts the overlaying app and its content is not vulnerable
+     * to UI redressing attacks.
+     *
+     * @see ActivityOptions#setAllowPassThroughOnTouchOutside
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    static final long ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT = 358129114L;
+
     private final ActivityRecord mActivityRecord;
     private final boolean mIsCompatEnabled;
     private final String mName;
@@ -42,13 +62,24 @@
     private InputWindowHandleWrapper mInputWindowHandleWrapper;
     private SurfaceControl mSurfaceControl;
 
-    ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord) {
+    ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord,
+            boolean appOptInTouchPassThrough) {
         mActivityRecord = activityRecord;
         mIsCompatEnabled = CompatChanges.isChangeEnabled(ENABLE_TOUCH_OPAQUE_ACTIVITIES,
                 mActivityRecord.getUid());
         mName = Integer.toHexString(System.identityHashCode(this)) + " ActivityRecordInputSink "
                 + mActivityRecord.mActivityComponent.flattenToShortString();
-        if (sourceRecord != null) {
+
+        if (sourceRecord == null) {
+            return;
+        }
+        // If the source activity has target sdk 36+, it is required to opt in to receive
+        // pass-through touches from the overlaying activity.
+        final boolean isTouchPassThroughOptInEnforced = CompatChanges.isChangeEnabled(
+                ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT,
+                sourceRecord.getUid());
+        if (!Flags.touchPassThroughOptIn() || !isTouchPassThroughOptInEnforced
+                || appOptInTouchPassThrough) {
             sourceRecord.mAllowedTouchUid = mActivityRecord.getUid();
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d29ff54..2ba300a 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -100,6 +100,7 @@
 import android.app.WaitResult;
 import android.app.WindowConfiguration;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -182,7 +183,7 @@
      * Feature flag for go/activity-security rules
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Disabled
     static final long ASM_RESTRICTIONS = 230590090L;
 
     private final ActivityTaskManagerService mService;
@@ -1028,6 +1029,7 @@
                 if (requestCode >= 0 && !sourceRecord.finishing) {
                     resultRecord = sourceRecord;
                 }
+                request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")");
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index f245efd..0e66629 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -255,8 +255,8 @@
                 mActivityRecord.getOverrideOrientation());
         final AppCompatCameraOverrides cameraOverrides =
                 mActivityRecord.mAppCompatController.getAppCompatCameraOverrides();
-        final AppCompatCameraPolicy cameraPolicy =
-                mActivityRecord.mAppCompatController.getAppCompatCameraPolicy();
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+                mActivityRecord);
         // Don't resize to split screen size when in book mode if letterbox position is centered
         return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
                 || cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed()
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index 51ef87d..3b023fe 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -72,6 +72,15 @@
 
     float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
             @NonNull Rect parentBounds) {
+        // If in camera compat mode, aspect ratio from the camera compat policy has priority over
+        // default letterbox aspect ratio.
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+                mActivityRecord);
+        if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio(
+                mActivityRecord)) {
+            return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
+        }
+
         final float letterboxAspectRatioOverride =
                 mAppCompatOverrides.getAppCompatAspectRatioOverrides()
                         .getFixedOrientationLetterboxAspectRatio(newParentConfig);
@@ -114,20 +123,20 @@
             return mTransparentPolicy.getInheritedMinAspectRatio();
         }
         final ActivityInfo info = mActivityRecord.info;
-        if (info.applicationInfo == null) {
-            return info.getMinAspectRatio();
-        }
         final AppCompatAspectRatioOverrides aspectRatioOverrides =
                 mAppCompatOverrides.getAppCompatAspectRatioOverrides();
         if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
             return aspectRatioOverrides.getUserMinAspectRatio();
         }
-        final DisplayContent displayContent = mActivityRecord.getDisplayContent();
-        final boolean shouldOverrideMinAspectRatioForCamera = displayContent != null
-                && displayContent.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(
-                        mActivityRecord);
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+                mActivityRecord);
+        final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
+                && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
         if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
                 && !shouldOverrideMinAspectRatioForCamera) {
+            if (mActivityRecord.isUniversalResizeable()) {
+                return 0;
+            }
             return info.getMinAspectRatio();
         }
 
@@ -170,6 +179,9 @@
         if (mTransparentPolicy.isRunning()) {
             return mTransparentPolicy.getInheritedMaxAspectRatio();
         }
+        if (mActivityRecord.isUniversalResizeable()) {
+            return 0;
+        }
         return mActivityRecord.info.getMaxAspectRatio();
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 5338c01..f6090eb 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -74,6 +76,12 @@
         }
     }
 
+    @Nullable
+    static AppCompatCameraPolicy getAppCompatCameraPolicy(@NonNull ActivityRecord activityRecord) {
+        return activityRecord.mDisplayContent != null
+                ? activityRecord.mDisplayContent.mAppCompatCameraPolicy : null;
+    }
+
     /**
      * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
      * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
@@ -167,12 +175,37 @@
                 : SCREEN_ORIENTATION_UNSPECIFIED;
     }
 
+    // TODO(b/369070416): have policies implement the same interface.
+    boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+        return (mDisplayRotationCompatPolicy != null
+                        && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(
+                                activity))
+                || (mCameraCompatFreeformPolicy != null
+                        && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation(
+                                activity));
+    }
+
+    // TODO(b/369070416): have policies implement the same interface.
+    boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+        return (mDisplayRotationCompatPolicy != null
+                        && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio(
+                                activity))
+                || (mCameraCompatFreeformPolicy != null
+                        && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio(
+                                activity));
+    }
+
+    // TODO(b/369070416): have policies implement the same interface.
     /**
-     * @return {@code true} if the Camera is active for the provided {@link ActivityRecord}.
+     * @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and
+     * any camera compat treatment could be triggered for the current windowing mode.
      */
-    boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
-        return mDisplayRotationCompatPolicy != null
-                && mDisplayRotationCompatPolicy.isCameraActive(activity, mustBeFullscreen);
+    private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
+        return (mDisplayRotationCompatPolicy != null
+                && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity,
+                        /* mustBeFullscreen */ true))
+                || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy
+                        .isCameraRunningAndWindowingModeEligible(activity));
     }
 
     @Nullable
@@ -183,12 +216,24 @@
         return null;
     }
 
+    // TODO(b/369070416): have policies implement the same interface.
+    float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+        float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null
+                ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
+                : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+        float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null
+                ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
+                : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+        return Math.max(displayRotationCompatPolicyAspectRatio,
+                cameraCompatFreeformPolicyAspectRatio);
+    }
+
     /**
      * Whether we should apply the min aspect ratio per-app override only when an app is connected
      * to the camera.
      */
     boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
-        return isCameraActive(activityRecord, /* mustBeFullscreen= */ true)
+        return isCameraRunningAndWindowingModeEligible(activityRecord)
                 && activityRecord.mAppCompatController.getAppCompatCameraOverrides()
                         .isOverrideMinAspectRatioForCameraEnabled();
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
index 42378aa..38c6de1 100644
--- a/services/core/java/com/android/server/wm/AppCompatConfiguration.java
+++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
@@ -290,6 +290,10 @@
     // is enabled and activity is connected to the camera in fullscreen.
     private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled;
 
+    // Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+    // for treatment is connected to the camera.
+    private float mCameraCompatAspectRatio;
+
     // Whether activity "refresh" in camera compatibility treatment is enabled.
     // See RefreshCallbackItem for context.
     private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
@@ -363,6 +367,8 @@
                         .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
         mIsCameraCompatSplitScreenAspectRatioEnabled = mContext.getResources().getBoolean(
                 R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
+        mCameraCompatAspectRatio = mContext.getResources().getFloat(
+                R.dimen.config_windowManagerCameraCompatAspectRatio);
         mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
 
@@ -1320,6 +1326,31 @@
     }
 
     /**
+     * Overrides aspect ratio to use when camera compat treatment is enabled and an activity
+     * eligible for treatment is connected to the camera.
+     */
+    void setCameraCompatAspectRatio(float aspectRatio) {
+        mCameraCompatAspectRatio = aspectRatio;
+    }
+
+    /**
+     * Which aspect ratio to use when camera compat treatment is enabled and an activity eligible
+     * for treatment is connected to the camera.
+     */
+    float getCameraCompatAspectRatio() {
+        return mCameraCompatAspectRatio;
+    }
+
+    /**
+     * Resets aspect ratio to use when camera compat treatment is enabled and an activity eligible
+     * for treatment is connected to the camera.
+     */
+    void resetCameraCompatAspectRatio() {
+        mCameraCompatAspectRatio = mContext.getResources().getFloat(R.dimen
+                .config_windowManagerCameraCompatAspectRatio);
+    }
+
+    /**
      * Checks whether rotation compat policy for immersive apps that prevents auto rotation
      * into non-optimal screen orientation while in fullscreen is enabled at build time. This is
      * used when we need to safely initialize a component before the {@link DeviceConfig} flag
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 173362c..6c344c6 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -16,7 +16,6 @@
 package com.android.server.wm;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 
 import com.android.server.wm.utils.OptPropFactory;
@@ -118,14 +117,6 @@
         return mAppCompatOverrides.getAppCompatResizeOverrides();
     }
 
-    @Nullable
-    AppCompatCameraPolicy getAppCompatCameraPolicy() {
-        if (mActivityRecord.mDisplayContent != null) {
-            return mActivityRecord.mDisplayContent.mAppCompatCameraPolicy;
-        }
-        return null;
-    }
-
     @NonNull
     AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
         return mAppCompatReachabilityPolicy;
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 7477c62..5bd4aeb 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -58,16 +58,17 @@
                 && displayContent.getIgnoreOrientationRequest();
         final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides
                 .getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride();
-        final boolean isCameraActive = displayContent != null
-                && displayContent.mAppCompatCameraPolicy.isCameraActive(mActivityRecord,
-                        /* mustBeFullscreen */ true);
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+                .getAppCompatCameraPolicy(mActivityRecord);
+        final boolean shouldCameraCompatControlOrientation = cameraPolicy != null
+                && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
         if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled
                 // Do not override orientation to fullscreen for camera activities.
                 // Fixed-orientation activities are rarely tested in other orientations, and it
                 // often results in sideways or stretched previews. As the camera compat treatment
                 // targets fixed-orientation activities, overriding the orientation disables the
                 // treatment.
-                && !isCameraActive) {
+                && !shouldCameraCompatControlOrientation) {
             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate)
                     + " for " + mActivityRecord + " is overridden to "
                     + screenOrientationToString(SCREEN_ORIENTATION_USER)
@@ -113,7 +114,7 @@
                 // often results in sideways or stretched previews. As the camera compat treatment
                 // targets fixed-orientation activities, overriding the orientation disables the
                 // treatment.
-                && !isCameraActive) {
+                && !shouldCameraCompatControlOrientation) {
             Slog.v(TAG, "Requested orientation  " + screenOrientationToString(candidate)
                     + " for " + mActivityRecord + " is overridden to "
                     + screenOrientationToString(SCREEN_ORIENTATION_USER));
@@ -192,8 +193,9 @@
                         + mActivityRecord);
                 return true;
             }
-            final AppCompatCameraPolicy cameraPolicy = mActivityRecord.mAppCompatController
-                    .getAppCompatCameraPolicy();
+
+            final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
+                    .getAppCompatCameraPolicy(mActivityRecord);
             if (cameraPolicy != null
                     && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) {
                 Slog.w(TAG, "Ignoring orientation update to "
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index d6caa1a..290e71d 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -29,6 +29,7 @@
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_180;
 
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -123,7 +124,7 @@
      * </ul>
      */
     @VisibleForTesting
-    boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
+    boolean isCameraCompatForFreeformEnabledForActivity(@NonNull ActivityRecord activity) {
         return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
                 ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
     }
@@ -170,6 +171,36 @@
         return true;
     }
 
+    boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+        return isCameraRunningAndWindowingModeEligible(activity);
+    }
+
+    boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
+        return activity.inFreeformWindowingMode()
+                && mCameraStateMonitor.isCameraRunningForActivity(activity);
+    }
+
+    boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+        // Camera compat should direct aspect ratio when in camera compat mode, unless an app has a
+        // different camera compat aspect ratio set: this allows per-app camera compat override
+        // aspect ratio to be smaller than the default.
+        return isInCameraCompatMode(activity) && !activity.mAppCompatController
+                .getAppCompatCameraOverrides().isOverrideMinAspectRatioForCameraEnabled();
+    }
+
+    private boolean isInCameraCompatMode(@NonNull ActivityRecord activity) {
+        return activity.mAppCompatController.getAppCompatCameraOverrides()
+                .getFreeformCameraCompatMode() != CAMERA_COMPAT_FREEFORM_NONE;
+    }
+
+    float getCameraCompatAspectRatio(@NonNull ActivityRecord activityRecord) {
+        if (shouldCameraCompatControlAspectRatio(activityRecord)) {
+            return activityRecord.mWmService.mAppCompatConfiguration.getCameraCompatAspectRatio();
+        }
+
+        return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+    }
+
     private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) {
         cameraActivity.recomputeConfiguration();
         cameraActivity.updateReportedConfigurationAndSend();
@@ -225,7 +256,7 @@
      */
     private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
         int orientation = activity.getRequestedConfigurationOrientation();
-        return shouldApplyFreeformTreatmentForCameraCompat(activity)
+        return isCameraCompatForFreeformEnabledForActivity(activity)
                 && mCameraStateMonitor.isCameraRunningForActivity(activity)
                 && orientation != ORIENTATION_UNDEFINED
                 && activity.inFreeformWindowingMode()
diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
index 1924691..3b2f723 100644
--- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
@@ -188,21 +188,21 @@
         }
 
         final ActivityInfo info = mActivityRecord.info;
-        if (info.applicationInfo == null) {
-            return info.getMinAspectRatio();
-        }
-
         final AppCompatAspectRatioOverrides aspectRatioOverrides =
                 mAppCompatOverrides.getAppCompatAspectRatioOverrides();
         if (shouldApplyUserMinAspectRatioOverride(task)) {
             return aspectRatioOverrides.getUserMinAspectRatio();
         }
 
-        final DisplayContent dc = task.mDisplayContent;
-        final boolean shouldOverrideMinAspectRatioForCamera = dc != null
-                && dc.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
+        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
+                mActivityRecord);
+        final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
+                && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
         if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
                 && !shouldOverrideMinAspectRatioForCamera) {
+            if (mActivityRecord.isUniversalResizeable()) {
+                return 0;
+            }
             return info.getMinAspectRatio();
         }
 
@@ -246,6 +246,9 @@
         if (mTransparentPolicy.isRunning()) {
             return mTransparentPolicy.getInheritedMaxAspectRatio();
         }
+        if (mActivityRecord.isUniversalResizeable()) {
+            return 0;
+        }
         return mActivityRecord.info.getMaxAspectRatio();
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e6f6215..1ac0bb0 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2156,6 +2156,11 @@
         }
         mDecorInsets.invalidate();
         mDecorInsets.mInfoForRotation[rotation].set(newInfo);
+        if (!mService.mDisplayEnabled) {
+            // There could be other pending changes during booting. It might be better to let the
+            // clients receive the new states earlier.
+            return true;
+        }
         return !sameConfigFrame;
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index efc3843..90f8b49 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -30,6 +30,7 @@
 import static android.view.Display.TYPE_INTERNAL;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT;
 
 import android.annotation.NonNull;
@@ -133,6 +134,11 @@
         return mLastReportedOrientation;
     }
 
+    float getCameraCompatAspectRatio(@NonNull ActivityRecord unusedActivity) {
+        // This policy does not apply camera compat aspect ratio by default, only via overrides.
+        return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+    }
+
     @ScreenOrientation
     private synchronized int getOrientationInternal() {
         if (!isTreatmentEnabledForDisplay()) {
@@ -271,7 +277,7 @@
 
     boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
         return isTreatmentEnabledForDisplay()
-                && isCameraActive(activity, /* mustBeFullscreen */ true)
+                && isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen */ true)
                 && activity.mAppCompatController.getAppCompatCameraOverrides()
                     .shouldForceRotateForCameraCompat();
     }
@@ -290,7 +296,17 @@
         return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true);
     }
 
-    boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
+    boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+        return isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen= */ true);
+    }
+
+    boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord unusedActivity) {
+        // This policy does not apply camera compat aspect ratio by default, only via overrides.
+        return false;
+    }
+
+    boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity,
+            boolean mustBeFullscreen) {
         // Checking windowing mode on activity level because we don't want to
         // apply treatment in case of activity embedding.
         return (!mustBeFullscreen || !activity.inMultiWindowMode())
@@ -299,7 +315,8 @@
 
     private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity,
             boolean mustBeFullscreen) {
-        return activity != null && isCameraActive(activity, mustBeFullscreen)
+        return activity != null
+                && isCameraRunningAndWindowingModeEligible(activity, mustBeFullscreen)
                 && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
                 // "locked" and "nosensor" values are often used by camera apps that can't
                 // handle dynamic changes so we shouldn't force rotate them.
@@ -428,6 +445,7 @@
     private boolean shouldOverrideMinAspectRatio(@NonNull ActivityRecord activityRecord) {
         return activityRecord.mAppCompatController.getAppCompatCameraOverrides()
                 .isOverrideMinAspectRatioForCameraEnabled()
-                        && isCameraActive(activityRecord, /* mustBeFullscreen= */ true);
+                        && isCameraRunningAndWindowingModeEligible(activityRecord,
+                                /* mustBeFullscreen= */ true);
     }
 }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index db0374e..e4fd523 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -815,8 +815,7 @@
                 if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
                     // It also invokes kill().
                     mDisplayContent.setRotationAnimation(null);
-                    mDisplayContent.mAppCompatCameraPolicy
-                            .onScreenRotationAnimationFinished();
+                    mDisplayContent.mAppCompatCameraPolicy.onScreenRotationAnimationFinished();
                 } else {
                     kill();
                 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 14f034b..edbc328 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1475,7 +1475,7 @@
             // The starting window should keep covering its task when a pure TaskFragment is added
             // because its bounds may not fill the task.
             final ActivityRecord top = getTopMostActivity();
-            if (top != null) {
+            if (top != null && !top.hasFixedRotationTransform()) {
                 top.associateStartingWindowWithTaskIfNeeded();
             }
         }
@@ -4707,8 +4707,13 @@
                         // If the moveToFront is a part of finishing transition, then make sure
                         // the z-order of tasks are up-to-date.
                         if (topActivity.mTransitionController.inFinishingTransition(topActivity)) {
-                            Transition.assignLayers(taskDisplayArea,
-                                    taskDisplayArea.getPendingTransaction());
+                            final SurfaceControl.Transaction tx =
+                                    taskDisplayArea.getPendingTransaction();
+                            Transition.assignLayers(taskDisplayArea, tx);
+                            final SurfaceControl leash = topActivity.getFixedRotationLeash();
+                            if (leash != null) {
+                                tx.setLayer(leash, topActivity.getLastLayer());
+                            }
                         }
                     }
                 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 06d8c37..6d7396f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1038,6 +1038,25 @@
         return 0;
     }
 
+    private int runSetCameraCompatAspectRatio(PrintWriter pw) throws RemoteException {
+        final float aspectRatio;
+        try {
+            String arg = getNextArgRequired();
+            aspectRatio = Float.parseFloat(arg);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: bad aspect ratio format " + e);
+            return -1;
+        } catch (IllegalArgumentException e) {
+            getErrPrintWriter().println(
+                    "Error: aspect ratio should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mAppCompatConfiguration.setCameraCompatAspectRatio(aspectRatio);
+        }
+        return 0;
+    }
+
     private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException {
         if (peekNextArg() == null) {
             getErrPrintWriter().println("Error: No arguments provided.");
@@ -1129,6 +1148,9 @@
                     runSetBooleanFlag(pw,
                             mAppCompatConfiguration::setCameraCompatRefreshCycleThroughStopEnabled);
                     break;
+                case "--cameraCompatAspectRatio":
+                    runSetCameraCompatAspectRatio(pw);
+                    break;
                 default:
                     getErrPrintWriter().println(
                             "Error: Unrecognized letterbox style option: " + arg);
@@ -1220,6 +1242,9 @@
                         mAppCompatConfiguration
                                 .resetCameraCompatRefreshCycleThroughStopEnabled();
                         break;
+                    case "cameraCompatAspectRatio":
+                        mAppCompatConfiguration.resetCameraCompatAspectRatio();
+                        break;
                     default:
                         getErrPrintWriter().println(
                                 "Error: Unrecognized letterbox style option: " + arg);
@@ -1330,6 +1355,7 @@
             mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled();
             mAppCompatConfiguration.resetCameraCompatRefreshEnabled();
             mAppCompatConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
+            mAppCompatConfiguration.resetCameraCompatAspectRatio();
         }
     }
 
@@ -1619,6 +1645,11 @@
         pw.println("        Whether activity \"refresh\" in camera compatibility treatment should");
         pw.println("        happen using the \"stopped -> resumed\" cycle rather than");
         pw.println("        \"paused -> resumed\" cycle.");
+        pw.println("      --cameraCompatAspectRatio aspectRatio");
+        pw.println("        Aspect ratio of letterbox for fixed-orientation camera apps, during ");
+        pw.println("        freeform camera compat mode. If aspectRatio <= "
+                + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+        pw.println("        it will be ignored.");
         pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
         pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
         pw.println("      |horizontalPositionMultiplier|verticalPositionMultiplier");
@@ -1627,7 +1658,8 @@
         pw.println("      |isTranslucentLetterboxingEnabled|isUserAppAspectRatioSettingsEnabled");
         pw.println("      |persistentPositionMultiplierForHorizontalReachability");
         pw.println("      |persistentPositionMultiplierForVerticalReachability");
-        pw.println("      |defaultPositionMultiplierForVerticalReachability]");
+        pw.println("      |defaultPositionMultiplierForVerticalReachability");
+        pw.println("      |cameraCompatAspectRatio]");
         pw.println("    Resets overrides to default values for specified properties separated");
         pw.println("    by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
         pw.println("    If no arguments provided, all values will be reset.");
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
index 8508802..b4e2544 100644
--- a/services/profcollect/src/com/android/server/profcollect/Utils.java
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -19,6 +19,7 @@
 import static com.android.server.profcollect.ProfcollectForwardingService.LOG_TAG;
 
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.provider.DeviceConfig;
 import android.util.Log;
 
@@ -42,7 +43,7 @@
         BackgroundThread.get().getThreadHandler().post(() -> {
             try {
                 mIProfcollect.trace_system(eventName);
-            } catch (RemoteException e) {
+            } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
             }
         });
@@ -56,7 +57,7 @@
         BackgroundThread.get().getThreadHandler().postDelayed(() -> {
             try {
                 mIProfcollect.trace_system(eventName);
-            } catch (RemoteException e) {
+            } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
             }
         }, delayMs);
@@ -73,10 +74,10 @@
                 mIProfcollect.trace_process(eventName,
                         processName,
                         durationMs);
-            } catch (RemoteException e) {
+            } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
             }
         });
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
index 90f6257..d00e2c6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.ActivityManager;
 import android.app.Instrumentation;
@@ -38,6 +39,9 @@
 import android.os.Message;
 import android.os.Messenger;
 import android.platform.test.annotations.AppModeSdkSandbox;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -46,15 +50,19 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.TestUtils;
+import com.android.server.am.Flags;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -68,6 +76,10 @@
 public class DisplayEventDeliveryTest {
     private static final String TAG = "DisplayEventDeliveryTest";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String NAME = TAG;
     private static final int WIDTH = 720;
     private static final int HEIGHT = 480;
@@ -149,7 +161,6 @@
             mExpectations.offer(event);
         }
 
-
         /**
          * Assert that there isn't any unexpected display event from the test activity
          */
@@ -189,19 +200,9 @@
     @Parameter(0)
     public int mDisplayCount;
 
-    /**
-     * True if running the test activity in cached mode
-     * False if running it in non-cached mode
-     */
-    @Parameter(1)
-    public boolean mCached;
-
-    @Parameters(name = "#{index}: {0} {1}")
+    @Parameters(name = "#{index}: {0}")
     public static Iterable<? extends Object> data() {
-        return Arrays.asList(new Object[][]{
-                {1, false}, {2, false}, {3, false}, {10, false},
-                {1, true}, {2, true}, {3, true}, {10, true}
-        });
+        return Arrays.asList(new Object[][]{ {1}, {2}, {3}, {10} });
     }
 
     private class TestHandler extends Handler {
@@ -289,20 +290,51 @@
     }
 
     /**
-     * Create virtual displays, change their configurations and release them
-     * mDisplays: the amount of virtual displays to be created
-     * mCached: true to run the test activity in cached mode; false in non-cached mode
+     * Return true if the freezer is enabled on this platform.
      */
-    @Test
-    public void testDisplayEvents() {
-        Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + mCached);
-        // Launch DisplayEventActivity and start listening to display events
-        launchTestActivity();
+    private boolean isAppFreezerEnabled() {
+        try {
+            return mActivityManager.getService().isAppFreezerEnabled();
+        } catch (Exception e) {
+            Log.e(TAG, "isAppFreezerEnabled() failed: " + e);
+            return false;
+        }
+    }
 
-        if (mCached) {
-            // The test activity in cached mode won't receive the pending display events
+    private void waitForProcessFreeze(int pid, long timeoutMs) {
+        // TODO: Add a listener to monitor freezer state changes.
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+                    () -> mActivityManager.isProcessFrozen(pid));
+        });
+    }
+
+    private void waitForProcessUnfreeze(int pid, long timeoutMs) {
+        // TODO: Add a listener to monitor freezer state changes.
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+                    () -> !mActivityManager.isProcessFrozen(pid));
+        });
+    }
+
+    /**
+     * Create virtual displays, change their configurations and release them.  The number of
+     * displays is set by the {@link #mDisplays} variable.
+     */
+    private void testDisplayEventsInternal(boolean cached, boolean frozen) {
+        Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen);
+        // Launch DisplayEventActivity and start listening to display events
+        int pid = launchTestActivity();
+
+        // The test activity in cached or frozen mode won't receive the pending display events.
+        if (cached) {
             makeTestActivityCached();
         }
+        if (frozen) {
+            makeTestActivityFrozen(pid);
+        }
 
         // Create new virtual displays
         for (int i = 0; i < mDisplayCount; i++) {
@@ -315,8 +347,8 @@
         }
 
         for (int i = 0; i < mDisplayCount; i++) {
-            if (mCached) {
-                // DISPLAY_ADDED should be deferred for cached process
+            if (cached || frozen) {
+                // DISPLAY_ADDED should be deferred for a cached or frozen process.
                 displayBundleAt(i).assertNoDisplayEvents();
             } else {
                 // DISPLAY_ADDED should arrive immediately for non-cached process
@@ -331,8 +363,8 @@
         }
 
         for (int i = 0; i < mDisplayCount; i++) {
-            if (mCached) {
-                // DISPLAY_CHANGED should be deferred for cached process
+            if (cached || frozen) {
+                // DISPLAY_CHANGED should be deferred for cached or frozen  process.
                 displayBundleAt(i).assertNoDisplayEvents();
             } else {
                 // DISPLAY_CHANGED should arrive immediately for non-cached process
@@ -340,10 +372,16 @@
             }
         }
 
-        if (mCached) {
-            // The test activity becomes non-cached and should receive the pending display events
+        // Unfreeze the test activity, if it was frozen.
+        if (frozen) {
+            makeTestActivityUnfrozen(pid);
+        }
+
+        if (cached || frozen) {
+            // Always ensure the test activity is not cached.
             bringTestActivityTop();
 
+            // The test activity becomes non-cached and should receive the pending display events
             for (int i = 0; i < mDisplayCount; i++) {
                 // The pending DISPLAY_ADDED & DISPLAY_CHANGED should arrive now
                 displayBundleAt(i).waitDisplayEvent(DISPLAY_ADDED);
@@ -363,9 +401,48 @@
     }
 
     /**
-     * Launch the test activity that would listen to display events
+     * Create virtual displays, change their configurations and release them.
      */
-    private void launchTestActivity() {
+    @Test
+    public void testDisplayEvents() {
+        testDisplayEventsInternal(false, false);
+    }
+
+    /**
+     * Create virtual displays, change their configurations and release them.  The display app is
+     * moved to cached and the test verifies that no events are delivered to the cached app.
+     */
+    @Test
+    public void testDisplayEventsCached() {
+        testDisplayEventsInternal(true, false);
+    }
+
+    /**
+     * Create virtual displays, change their configurations and release them.  The display app is
+     * frozen and the test verifies that no events are delivered to the frozen app.
+     */
+    @RequiresFlagsEnabled(Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN)
+    @Test
+    public void testDisplayEventsFrozen() {
+        assumeTrue(isAppFreezerEnabled());
+        testDisplayEventsInternal(false, true);
+    }
+
+    /**
+     * Create virtual displays, change their configurations and release them.  The display app is
+     * cached and frozen and the test verifies that no events are delivered to the app.
+     */
+    @RequiresFlagsEnabled(Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN)
+    @Test
+    public void testDisplayEventsCachedFrozen() {
+        assumeTrue(isAppFreezerEnabled());
+        testDisplayEventsInternal(true, true);
+    }
+
+    /**
+     * Launch the test activity that would listen to display events. Return its process ID.
+     */
+    private int launchTestActivity() {
         Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
         intent.putExtra(TEST_MESSENGER, mMessenger);
@@ -377,6 +454,18 @@
                 },
                 android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
         waitLatch(mLatchActivityLaunch);
+
+        try {
+            String cmd = "pidof " + TEST_PACKAGE;
+            String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
+            return Integer.parseInt(result.trim());
+        } catch (IOException e) {
+            fail("failed to get pid of test package");
+            return 0;
+        } catch (NumberFormatException e) {
+            fail("failed to parse pid " + e);
+            return 0;
+        }
     }
 
     /**
@@ -415,6 +504,45 @@
         waitLatch(mLatchActivityCached);
     }
 
+    // Sleep, ignoring interrupts.
+    private void pause(int s) {
+        try { Thread.sleep(s * 1000); } catch (Exception e) { }
+    }
+
+    /**
+     * Freeze the test activity.
+     */
+    private void makeTestActivityFrozen(int pid) {
+        // The delay here is meant to allow pending binder transactions to drain.  A process
+        // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
+        // process more than a few times will result in the system killing the process.
+        pause(5);
+        try {
+            String cmd = "am freeze --sticky ";
+            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
+        } catch (IOException e) {
+            fail(e.toString());
+        }
+        // Wait for the freeze to complete in the kernel and for the frozen process
+        // notification to settle out.
+        waitForProcessFreeze(pid, 5 * 1000);
+    }
+
+    /**
+     * Freeze the test activity.
+     */
+    private void makeTestActivityUnfrozen(int pid) {
+        try {
+            String cmd = "am unfreeze --sticky ";
+            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
+        } catch (IOException e) {
+            fail(e.toString());
+        }
+        // Wait for the freeze to complete in the kernel and for the frozen process
+        // notification to settle out.
+        waitForProcessUnfreeze(pid, 5 * 1000);
+    }
+
     /**
      * Create a virtual display
      *
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index 14cb22d..efc2d97 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -16,12 +16,20 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+
+import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
@@ -36,8 +44,12 @@
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 @Presubmit
 @SmallTest
@@ -45,6 +57,17 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
+    public MockitoRule mockitorule = MockitoJUnit.rule();
+
+    @Mock
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+    }
 
     @Test
     public void testCombineAuthenticatorBundles_withKeyDeviceCredential_andKeyAuthenticators() {
@@ -162,28 +185,39 @@
 
     @Test
     public void testIsValidAuthenticatorConfig() {
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.EMPTY_SET));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.EMPTY_SET));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_STRONG));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_WEAK));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_WEAK));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_WEAK));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_CONVENIENCE));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE
+        assertFalse(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_CONVENIENCE
                 | Authenticators.DEVICE_CREDENTIAL));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MAX_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MAX_STRENGTH));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MIN_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MIN_STRENGTH));
+
+        assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig(
+                        mContext, Authenticators.MANDATORY_BIOMETRICS));
+
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.MANDATORY_BIOMETRICS));
 
         // The rest of the bits are not allowed to integrate with the public APIs
         for (int i = 8; i < 32; i++) {
@@ -192,7 +226,7 @@
                     || authenticator == Authenticators.MANDATORY_BIOMETRICS) {
                 continue;
             }
-            assertFalse(Utils.isValidAuthenticatorConfig(1 << i));
+            assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i));
         }
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 62e5b9a..45cd571 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -31,6 +31,12 @@
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
 
+import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_COOLDOWN;
+import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_FLAG_SILENT;
+import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_GROUP_ALERT;
+import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_NOT_MUTED;
+import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_OTHER_INSISTENT_PLAYING;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -106,6 +112,7 @@
 import com.android.internal.config.sysui.TestableFlagResolver;
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.InstanceIdSequenceFake;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.IntPair;
 import com.android.server.UiServiceTestCase;
 import com.android.server.lights.LightsManager;
@@ -1276,7 +1283,8 @@
         verifyNeverBeep();
         assertFalse(r.isInterruptive());
         assertEquals(-1, r.getLastAudiblyAlertedMs());
-        assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS,
+                true)).isEqualTo(MUTE_REASON_FLAG_SILENT);
     }
 
     @Test
@@ -1295,7 +1303,8 @@
         verifyNeverBeep();
         assertFalse(r.isInterruptive());
         assertEquals(-1, r.getLastAudiblyAlertedMs());
-        assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS,
+                true)).isEqualTo(MUTE_REASON_GROUP_ALERT);
     }
 
     @Test
@@ -1861,7 +1870,9 @@
         verifyBeepLooped();
 
         NotificationRecord interrupter = getBeepyOtherNotification();
-        assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS));
+        assertThat(
+                mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS,
+                true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING);
         mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
 
         verifyBeep(1);
@@ -1879,16 +1890,16 @@
         ringtoneChannel.enableVibration(true);
         NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
         mService.addNotification(ringtoneNotification);
-        assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
-            DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED);
         mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
         verifyBeepLooped();
         verifyDelayedVibrateLooped();
         Mockito.reset(mVibrator);
         Mockito.reset(mRingtonePlayer);
 
-        assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
-            DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED);
         mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
 
         // beep wasn't reset
@@ -1907,8 +1918,8 @@
         ringtoneChannel.enableVibration(true);
         NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
         mService.addNotification(ringtoneNotification);
-        assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
-            DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED);
         mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
         verifyBeepLooped();
         verifyDelayedVibrateLooped();
@@ -1930,8 +1941,8 @@
         ringtoneChannel.enableVibration(true);
         NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
         mService.addNotification(ringtoneNotification);
-        assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
-            DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED);
         mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
         verifyBeepLooped();
         verifyNeverVibrate();
@@ -1951,14 +1962,15 @@
             new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build());
         ringtoneChannel.enableVibration(true);
         NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true);
-        assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
-            DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED);
 
         mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS);
         verifyVibrateLooped();
 
         NotificationRecord interrupter = getBuzzyOtherNotification();
-        assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS));
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(interrupter,
+                DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING);
         mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
 
         verifyVibrate(1);
@@ -2260,10 +2272,13 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-        verifyBeepVolume(0.0f);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
+        assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, true))
+                .isEqualTo(MUTE_REASON_COOLDOWN);
 
-        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+        verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt());
         assertEquals(-1, r.getLastAudiblyAlertedMs());
     }
 
@@ -2305,8 +2320,9 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
-        verifyBeepVolume(0.0f);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
         assertEquals(-1, r3.getLastAudiblyAlertedMs());
@@ -2381,9 +2397,10 @@
                 false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
 
         // update should beep at 0% volume
-        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
         assertEquals(-1, r2.getLastAudiblyAlertedMs());
-        verifyBeepVolume(0.0f);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         // Use different package for next notifications
         NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
@@ -2392,8 +2409,9 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
-        verifyBeepVolume(0.0f);
+        buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
         assertEquals(-1, r3.getLastAudiblyAlertedMs());
@@ -2493,8 +2511,9 @@
 
         // Regular notification: should beep at 0% volume
         NotificationRecord r = getBeepyNotification();
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-        verifyBeepVolume(0.0f);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
         assertEquals(-1, r.getLastAudiblyAlertedMs());
         Mockito.reset(mRingtonePlayer);
 
@@ -2525,8 +2544,9 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
-        verifyBeepVolume(0.0f);
+        buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         // Set important conversation
         mChannel.setImportantConversation(true);
@@ -2751,9 +2771,10 @@
         Mockito.reset(mRingtonePlayer);
 
         // next update at 0% volume
-        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
         assertEquals(-1, summary.getLastAudiblyAlertedMs());
-        verifyBeepVolume(0.0f);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
     }
@@ -2823,9 +2844,10 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
         assertEquals(-1, r2.getLastAudiblyAlertedMs());
-        verifyBeepVolume(0.0f);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
         // Use different package for next notifications
         NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
@@ -2891,6 +2913,94 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_groupAlertSummary() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        // child should beep at 0% volume
+        NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertFalse(child.isInterruptive());
+        assertEquals(-1, child.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // child should beep at 0% volume
+        child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertFalse(child.isInterruptive());
+        assertEquals(-1, child.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // summary 100% volume (GROUP_ALERT_SUMMARY)
+        NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY);
+        summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+        Mockito.reset(mRingtonePlayer);
+
+        // next update at 50% volume because only summary was tracked as alerting
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+        verifyBeepVolume(0.5f);
+
+        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
+    public void testBeepVolume_politeNotif_groupAlertChildren() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        // summary 0% volume (GROUP_ALERT_CHILDREN)
+        NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+        summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertFalse(summary.isInterruptive());
+        assertEquals(-1, summary.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // child should beep at 100% volume
+        NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        assertNotEquals(-1, child.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+        Mockito.reset(mRingtonePlayer);
+
+        // child should beep at 50% volume
+        child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN);
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        assertNotEquals(-1, child.getLastAudiblyAlertedMs());
+        verifyBeepVolume(0.5f);
+        Mockito.reset(mRingtonePlayer);
+
+        // child should beep at 0% volume
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertTrue(child.isInterruptive());
+        assertEquals(-1, child.getLastAudiblyAlertedMs());
+
+        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testVibrationIntensity_politeNotif() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
@@ -2914,8 +3024,9 @@
         Mockito.reset(vibratorHelper);
 
         // 2nd update should buzz at 0% intensity
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-        verify(vibratorHelper, times(1)).scale(any(), eq(0.0f));
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyNeverVibrate();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
     }
 
     @Test
@@ -3007,10 +3118,11 @@
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS);
-        verifyBeepVolume(0.0f);
+        int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS);
+        verifyNeverBeep();
+        assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN);
 
-        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+        verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt());
         assertEquals(-1, r.getLastAudiblyAlertedMs());
     }
 
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 44770d2..bbf2cbd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -193,6 +193,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
 import android.app.PendingIntent;
 import android.app.Person;
 import android.app.RemoteInput;
@@ -655,7 +656,8 @@
         when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt()))
                 .thenReturn(INVALID_TASK_ID);
         mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
-        when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId });
+        when(mUm.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
+        when(mUmInternal.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
         when(mAmi.getCurrentUserId()).thenReturn(mUserId);
 
         when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true);
@@ -15971,6 +15973,57 @@
         assertThat(updatedRule.getValue().isEnabled()).isFalse();
     }
 
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+
+        // The caller will supply states with "wrong" hasPriorityChannels.
+        int stateBlockingPriorityChannels = Policy.policyState(false, false);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, stateBlockingPriorityChannels, 0), false);
+
+        // hasPriorityChannels is untouched and allowPriorityChannels was updated.
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, false));
+
+        // Same but setting allowPriorityChannels to true.
+        int stateAllowingPriorityChannels = Policy.policyState(false, true);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(2, 0, 0, 0, stateAllowingPriorityChannels, 0), false);
+
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(2);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+        mService.setCallerIsNormalPackage();
+
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, Policy.policyState(false, false), 0), false);
+
+        // Policy was updated but the attempt to change state was ignored (it's a @hide API).
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
     /** Prepares for a zen-related test that uses the real {@link ZenModeHelper}. */
     private void setUpRealZenTest() throws Exception {
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 84c4f62..5709d88 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -1010,7 +1010,39 @@
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI)
-    public void testConfigXml_manualRule_upgradeWhenExisting() throws Exception {
+    public void testConfigXml_manualRuleWithoutCondition_upgradeWhenExisting() throws Exception {
+        // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much
+        // data on it because it's meant to indicate that the manual rule is on by merely existing.
+        ZenModeConfig config = new ZenModeConfig();
+        config.manualRule = new ZenModeConfig.ZenRule();
+        config.manualRule.enabled = true;
+        config.manualRule.pkg = "android";
+        config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        config.manualRule.conditionId = null;
+        config.manualRule.enabler = "test";
+
+        // write out entire config xml
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig fromXml = readConfigXml(bais);
+
+
+        // The result should be valid and contain a manual rule; the rule should have a non-null
+        // ZenPolicy and a condition whose state is true. The conditionId should be default.
+        assertThat(fromXml.isValid()).isTrue();
+        assertThat(fromXml.manualRule).isNotNull();
+        assertThat(fromXml.manualRule.zenPolicy).isNotNull();
+        assertThat(fromXml.manualRule.condition).isNotNull();
+        assertThat(fromXml.manualRule.condition.state).isEqualTo(STATE_TRUE);
+        assertThat(fromXml.manualRule.conditionId).isEqualTo(Uri.EMPTY);
+        assertThat(fromXml.manualRule.enabler).isEqualTo("test");
+        assertThat(fromXml.isManualActive()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    public void testConfigXml_manualRuleWithCondition_upgradeWhenExisting() throws Exception {
         // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much
         // data on it because it's meant to indicate that the manual rule is on by merely existing.
         ZenModeConfig config = new ZenModeConfig();
@@ -1029,6 +1061,7 @@
 
         // The result should have a manual rule; it should have a non-null ZenPolicy and a condition
         // whose state is true. The conditionId and enabler data should also be preserved.
+        assertThat(fromXml.isValid()).isTrue();
         assertThat(fromXml.manualRule).isNotNull();
         assertThat(fromXml.manualRule.zenPolicy).isNotNull();
         assertThat(fromXml.manualRule.condition).isNotNull();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
index 5787780..4cd75d5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
@@ -308,6 +308,8 @@
                     // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE
                 case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE
                 case "android:activity.animAbortListener": // KEY_ANIM_ABORT_LISTENER
+                case "android.activity.allowPassThroughOnTouchOutside":
+                    // KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE
                     // Existing keys
 
                     break;
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 92205f39..65736cb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -186,7 +186,8 @@
 
     void setTopActivityCameraActive(boolean enabled) {
         doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
-                .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true));
+                .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()),
+                        /* mustBeFullscreen= */ eq(true));
     }
 
     void setTopActivityEligibleForOrientationOverride(boolean enabled) {
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 dbcef10..a8ccf95 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -26,6 +26,7 @@
 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -35,6 +36,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.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
@@ -247,8 +249,8 @@
 
         assertTrue(mActivity.info
                 .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT));
-        assertFalse(mCameraCompatFreeformPolicy
-                .shouldApplyFreeformTreatmentForCameraCompat(mActivity));
+        assertFalse(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity(
+                mActivity));
     }
 
     @Test
@@ -256,8 +258,8 @@
     public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
-        assertTrue(mCameraCompatFreeformPolicy
-                .shouldApplyFreeformTreatmentForCameraCompat(mActivity));
+        assertTrue(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity(
+                mActivity));
     }
 
     @Test
@@ -303,6 +305,49 @@
         assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
     }
 
+    @Test
+    public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
+        configureActivity(SCREEN_ORIENTATION_FULL_USER);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity);
+
+        assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
+                mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+                /* delta= */ 0.001);
+    }
+
+    @Test
+    public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        final float configAspectRatio = 1.5f;
+        mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity);
+
+        assertEquals(configAspectRatio,
+                mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+                /* delta= */ 0.001);
+    }
+
+
+    @Test
+    public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        final float configAspectRatio = 1.5f;
+        mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+        doReturn(true).when(mActivity.mAppCompatController.getAppCompatCameraOverrides())
+                .isOverrideMinAspectRatioForCameraEnabled();
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity);
+
+        assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
+                mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
+                /* delta= */ 0.001);
+    }
+
     private void configureActivity(@ScreenOrientation int activityOrientation) {
         configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 8cf593f..35c9e3f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -544,39 +544,35 @@
     }
 
     @Test
-    public void testIsCameraActiveWhenCallbackInvokedNoMultiWindow_returnTrue() {
+    public void testShouldCameraCompatControlOrientationWhenInvokedNoMultiWindow_returnTrue() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertTrue(
-                mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+        assertTrue(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
     }
 
     @Test
-    public void testIsCameraActiveWhenCallbackNotInvokedNoMultiWindow_returnFalse() {
+    public void testShouldCameraCompatControlOrientationWhenNotInvokedNoMultiWindow_returnFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
-        assertFalse(
-                mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+        assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
     }
 
     @Test
-    public void testIsCameraActiveWhenCallbackNotInvokedMultiWindow_returnFalse() {
+    public void testShouldCameraCompatControlOrientationWhenNotInvokedMultiWindow_returnFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         when(mActivity.inMultiWindowMode()).thenReturn(true);
 
-        assertFalse(
-                mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+        assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
     }
 
     @Test
-    public void testIsCameraActiveWhenCallbackInvokedMultiWindow_returnFalse() {
+    public void testShouldCameraCompatControlOrientationWhenInvokedMultiWindow_returnFalse() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         when(mActivity.inMultiWindowMode()).thenReturn(true);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertFalse(
-                mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true));
+        assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity));
     }
 
     private void configureActivity(@ScreenOrientation int activityOrientation) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 8fa4667..adc969c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4849,6 +4849,39 @@
     }
 
     @Test
+    public void testUniversalResizeable() {
+        mWm.mConstants.mIgnoreActivityOrientationRequest = true;
+        setUpApp(mDisplayContent);
+        final float maxAspect = 1.8f;
+        final float minAspect = 1.5f;
+        prepareLimitedBounds(mActivity, maxAspect, minAspect,
+                ActivityInfo.SCREEN_ORIENTATION_LOCKED, true /* isUnresizable */);
+
+        assertTrue(mActivity.isUniversalResizeable());
+        assertTrue(mActivity.isResizeable());
+        assertFalse(mActivity.shouldCreateAppCompatDisplayInsets());
+        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation());
+        assertEquals(mActivity.getTask().getBounds(), mActivity.getBounds());
+        final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController
+                .getAppCompatAspectRatioPolicy();
+        assertEquals(0, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */);
+        assertEquals(0, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */);
+
+        // Compat override can still take effect.
+        final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                mActivity.mAppCompatController.getAppCompatAspectRatioOverrides();
+        spyOn(aspectRatioOverrides);
+        doReturn(true).when(aspectRatioOverrides).shouldOverrideMinAspectRatio();
+        assertEquals(minAspect, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */);
+
+        // User override can still take effect.
+        doReturn(true).when(aspectRatioOverrides).shouldApplyUserMinAspectRatioOverride();
+        assertFalse(mActivity.isResizeable());
+        assertEquals(maxAspect, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */);
+        assertNotEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation());
+    }
+
+    @Test
     public void testClearSizeCompat_resetOverrideConfig() {
         final int origDensity = 480;
         final int newDensity = 520;
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 3e226cc..92effe0 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -19426,4 +19426,48 @@
                 return "UNKNOWN(" + state + ")";
         }
     }
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing. {@code null} to reset.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+    public void setTestEuiccUiComponent(@Nullable ComponentName componentName) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "setTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            telephony.setTestEuiccUiComponent(componentName);
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "setTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing. {@code null} if not available.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @Nullable
+    public ComponentName getTestEuiccUiComponent() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "getTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            return telephony.getTestEuiccUiComponent();
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "getTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index 44d3fca..567314b 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -128,6 +128,12 @@
     /** APN type for RCS (Rich Communication Services). */
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int TYPE_RCS = ApnTypes.RCS;
+    /** APN type for OEM_PAID networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PAID = 1 << 16; // TODO(b/366194627): ApnTypes.OEM_PAID;
+    /** APN type for OEM_PRIVATE networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PRIVATE = 1 << 17; // TODO(b/366194627): ApnTypes.OEM_PRIVATE;
 
     /** @hide */
     @IntDef(flag = true, prefix = {"TYPE_"}, value = {
@@ -146,7 +152,9 @@
             TYPE_BIP,
             TYPE_VSIM,
             TYPE_ENTERPRISE,
-            TYPE_RCS
+            TYPE_RCS,
+            TYPE_OEM_PAID,
+            TYPE_OEM_PRIVATE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ApnType {
@@ -375,6 +383,27 @@
     @SystemApi
     public static final String TYPE_RCS_STRING = "rcs";
 
+    /**
+     * APN type for OEM_PAID networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+
+    /**
+     * APN type for OEM_PRIVATE networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
 
     /** @hide */
     @IntDef(prefix = { "AUTH_TYPE_" }, value = {
@@ -489,6 +518,8 @@
         APN_TYPE_STRING_MAP.put(TYPE_VSIM_STRING, TYPE_VSIM);
         APN_TYPE_STRING_MAP.put(TYPE_BIP_STRING, TYPE_BIP);
         APN_TYPE_STRING_MAP.put(TYPE_RCS_STRING, TYPE_RCS);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PAID_STRING, TYPE_OEM_PAID);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PRIVATE_STRING, TYPE_OEM_PRIVATE);
 
         APN_TYPE_INT_MAP = new ArrayMap<>();
         APN_TYPE_INT_MAP.put(TYPE_DEFAULT, TYPE_DEFAULT_STRING);
@@ -507,6 +538,8 @@
         APN_TYPE_INT_MAP.put(TYPE_VSIM, TYPE_VSIM_STRING);
         APN_TYPE_INT_MAP.put(TYPE_BIP, TYPE_BIP_STRING);
         APN_TYPE_INT_MAP.put(TYPE_RCS, TYPE_RCS_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PAID, TYPE_OEM_PAID_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PRIVATE, TYPE_OEM_PRIVATE_STRING);
 
         PROTOCOL_STRING_MAP = new ArrayMap<>();
         PROTOCOL_STRING_MAP.put("IP", PROTOCOL_IP);
@@ -2383,7 +2416,8 @@
         public ApnSetting build() {
             if ((mApnTypeBitmask & (TYPE_DEFAULT | TYPE_MMS | TYPE_SUPL | TYPE_DUN | TYPE_HIPRI
                     | TYPE_FOTA | TYPE_IMS | TYPE_CBS | TYPE_IA | TYPE_EMERGENCY | TYPE_MCX
-                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS)) == 0
+                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS | TYPE_OEM_PAID
+                    | TYPE_OEM_PRIVATE)) == 0
                 || TextUtils.isEmpty(mApnName) || TextUtils.isEmpty(mEntryName)) {
                 return null;
             }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index e57c207..7f25ef2 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3017,6 +3017,14 @@
     boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis);
 
     /**
+     * This API can be used by only CTS to control ingoring cellular service state event.
+     *
+     * @param enabled Whether to enable boolean config.
+     * @return {@code true} if the value is set successfully, {@code false} otherwise.
+     */
+    boolean setSatelliteIgnoreCellularServiceState(in boolean enabled);
+
+    /**
      * This API can be used by only CTS to update satellite pointing UI app package and class names.
      *
      * @param packageName The package name of the satellite pointing UI app.
@@ -3409,4 +3417,20 @@
      * @hide
      */
     boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name);
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing
+     * @hide
+     */
+    void setTestEuiccUiComponent(in ComponentName componentName);
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing.
+     * @hide
+     */
+    ComponentName getTestEuiccUiComponent();
 }
diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
index 82de070..8b65efd 100644
--- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
index 4ffb11a..3382c1e 100644
--- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
index 0fa4d07..e941e79 100644
--- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
index 4d9fefb..4e06dca 100644
--- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index b879c54..0cadd68 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- enable AOD -->
         <option name="set-secure-setting" key="doze_always_on" value="1" />
         <!-- prevents the phone from restarting -->
diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
index 04b312a..f32e8bed 100644
--- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
index 8acdabc..68ae4f1 100644
--- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index 91ece21..ec186723 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -12,6 +12,10 @@
         <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on"/>
+        <!-- Turns off Wi-fi -->
+        <option name="wifi" value="off"/>
+        <!-- Turns off Bluetooth -->
+        <option name="bluetooth" value="off"/>
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true"/>
         <!-- set WM tracing verbose level to all -->