Merge "Simplify nav mode change in InputMethodServiceTest" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 071cc65..e5c059e 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -84,7 +84,7 @@
         "android.view.inputmethod.flags-aconfig-java",
         "android.webkit.flags-aconfig-java",
         "android.widget.flags-aconfig-java",
-        "android.xr.flags-aconfig-java",
+        "android.xr.flags-aconfig-java-export",
         "art_exported_aconfig_flags_lib",
         "backstage_power_flags_lib",
         "backup_flags_lib",
@@ -989,15 +989,22 @@
 // XR
 aconfig_declarations {
     name: "android.xr.flags-aconfig",
-    package: "android.xr",
     container: "system",
+    exportable: true,
+    package: "android.xr",
     srcs: ["core/java/android/content/pm/xr.aconfig"],
 }
 
 java_aconfig_library {
-    name: "android.xr.flags-aconfig-java",
+    name: "android.xr.flags-aconfig-java-export",
     aconfig_declarations: "android.xr.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    mode: "exported",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
 
 // android.app
diff --git a/Android.bp b/Android.bp
index 9d3b64d..303fa2c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -583,6 +583,7 @@
         "documents-ui-compat-config",
         "calendar-provider-compat-config",
         "contacts-provider-platform-compat-config",
+        "SystemUI-core-compat-config",
     ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
         "true": [],
         default: [
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 9871d71..ab8131b 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -264,6 +264,8 @@
     @GuardedBy("mCarrierPrivilegedLock")
     private boolean mHaveCarrierPrivilegedApps;
 
+    private final boolean mHasFeatureTelephonySubscription;
+
     /** List of carrier-privileged apps that should be excluded from standby */
     @GuardedBy("mCarrierPrivilegedLock")
     private List<String> mCarrierPrivilegedApps;
@@ -603,6 +605,8 @@
         mContext = mInjector.getContext();
         mHandler = new AppStandbyHandler(mInjector.getLooper());
         mPackageManager = mContext.getPackageManager();
+        mHasFeatureTelephonySubscription = mPackageManager.hasSystemFeature(
+                PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
 
         DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver();
         IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING);
@@ -1515,7 +1519,7 @@
         }
 
         // Check this last, as it can be the most expensive check
-        if (isCarrierApp(packageName)) {
+        if (mHasFeatureTelephonySubscription && isCarrierApp(packageName)) {
             return STANDBY_BUCKET_EXEMPTED;
         }
 
diff --git a/api/OWNERS b/api/OWNERS
index 965093c..f2bcf13 100644
--- a/api/OWNERS
+++ b/api/OWNERS
@@ -9,4 +9,4 @@
 per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 
 # For metalava team to disable lint checks in platform
-per-file Android.bp = aurimas@google.com,emberrose@google.com
+per-file Android.bp = aurimas@google.com
diff --git a/cmds/am/am.sh b/cmds/am/am.sh
index 76ec214..f099be3 100755
--- a/cmds/am/am.sh
+++ b/cmds/am/am.sh
@@ -1,11 +1,10 @@
 #!/system/bin/sh
 
-# set to top-app process group
-settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true
-
 if [ "$1" != "instrument" ] ; then
     cmd activity "$@"
 else
+    # set to top-app process group for instrument
+    settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true
     base=/system
     export CLASSPATH=$base/framework/am.jar
     exec app_process $base/bin com.android.commands.am.Am "$@"
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 6310d32..696bc82 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.app.backup.BackupManager;
 import android.app.backup.BackupManagerMonitor;
 import android.app.backup.BackupProgress;
@@ -73,6 +74,8 @@
             "Error: Could not access the backup transport.  Is the system running?";
     private static final String PM_NOT_RUNNING_ERR =
             "Error: Could not access the Package Manager.  Is the system running?";
+    private static final String INVALID_USER_ID_ERR_TEMPLATE =
+            "Error: Invalid user id (%d).\n";
 
     private String[] mArgs;
     private int mNextArg;
@@ -104,6 +107,11 @@
         mArgs = args;
         mNextArg = 0;
         int userId = parseUserId();
+        if (userId < 0) {
+            System.err.printf(INVALID_USER_ID_ERR_TEMPLATE, userId);
+            return;
+        }
+
         String op = nextArg();
         Slog.v(TAG, "Running " + op + " for user:" + userId);
 
@@ -955,12 +963,15 @@
 
     private int parseUserId() {
         String arg = nextArg();
-        if ("--user".equals(arg)) {
-            return UserHandle.parseUserArg(nextArg());
-        } else {
+        if (!"--user".equals(arg)) {
             mNextArg--;
             return UserHandle.USER_SYSTEM;
         }
+        int userId = UserHandle.parseUserArg(nextArg());
+        if (userId == UserHandle.USER_CURRENT) {
+            userId = ActivityManager.getCurrentUser();
+        }
+        return userId;
     }
 
     private static void showUsage() {
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index b43905b..844e52c 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -441,7 +441,7 @@
             numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize);
             for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) {
                 const auto& event = buffer[i];
-                if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) {
+                if (event.header.type == DisplayEventType::DISPLAY_EVENT_HOTPLUG) {
                     SLOGV("Hotplug received");
 
                     if (!event.hotplug.connected) {
diff --git a/cmds/uinput/tests/Android.bp b/cmds/uinput/tests/Android.bp
index e728bd2..516de33 100644
--- a/cmds/uinput/tests/Android.bp
+++ b/cmds/uinput/tests/Android.bp
@@ -18,3 +18,17 @@
         "device-tests",
     ],
 }
+
+android_ravenwood_test {
+    name: "UinputTestsRavenwood",
+    srcs: [
+        "src/**/*.java",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "frameworks-base-testutils",
+        "platform-test-annotations",
+        "truth",
+        "uinput",
+    ],
+}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index e8ff546..9e9e3c2 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1806,17 +1806,17 @@
   }
 
   public class InputSettings {
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context);
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context);
+    method public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context);
+    method public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context);
     method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysDelay(@NonNull android.content.Context);
     method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysTimeout(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") public static boolean isAccessibilityMouseKeysEnabled(@NonNull android.content.Context);
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context);
+    method public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context);
     method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static boolean isRepeatKeysEnabled(@NonNull android.content.Context);
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int);
+    method @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityMouseKeysEnabled(@NonNull android.content.Context, boolean);
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int);
-    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean);
+    method @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int);
+    method @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float);
     method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysDelay(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysEnabled(@NonNull android.content.Context, boolean);
diff --git a/core/java/android/accessibilityservice/OWNERS b/core/java/android/accessibilityservice/OWNERS
index 1265dfa..dac64f4 100644
--- a/core/java/android/accessibilityservice/OWNERS
+++ b/core/java/android/accessibilityservice/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 # Android Accessibility Framework owners
 include /services/accessibility/OWNERS
\ No newline at end of file
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 82c746a..b8c20bd 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -2230,6 +2230,16 @@
         return mLaunchCookie;
     }
 
+    /**
+     * Set the ability for the current transition/animation to work cross-task.
+     * @param allowTaskOverride true to allow cross-task use, otherwise false.
+     *
+     * @hide
+     */
+    public ActivityOptions setOverrideTaskTransition(boolean allowTaskOverride) {
+        this.mOverrideTaskTransition = allowTaskOverride;
+        return this;
+    }
 
     /** @hide */
     public boolean getOverrideTaskTransition() {
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 2dead56..f2e7e85 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -17,7 +17,6 @@
 package android.app;
 
 import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
-import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
 import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
 import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED;
 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -1146,12 +1145,16 @@
         }
     }
 
-    private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
-            createSystemCacheKey("get_packages_for_uid");
-    private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
-            mGetPackagesForUidCache =
-            new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
-                1024, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) {
+    private static final String CACHE_KEY_PACKAGES_FOR_UID_API = "get_packages_for_uid";
+
+    /** @hide */
+    @VisibleForTesting
+    public static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
+            sGetPackagesForUidCache = new PropertyInvalidatedCache<>(
+                new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+                .maxEntries(1024).api(CACHE_KEY_PACKAGES_FOR_UID_API).cacheNulls(true),
+                CACHE_KEY_PACKAGES_FOR_UID_API, null) {
+
                 @Override
                 public GetPackagesForUidResult recompute(Integer uid) {
                     try {
@@ -1170,17 +1173,17 @@
 
     @Override
     public String[] getPackagesForUid(int uid) {
-        return mGetPackagesForUidCache.query(uid).value();
+        return sGetPackagesForUidCache.query(uid).value();
     }
 
     /** @hide */
     public static void disableGetPackagesForUidCache() {
-        mGetPackagesForUidCache.disableLocal();
+        sGetPackagesForUidCache.disableLocal();
     }
 
     /** @hide */
     public static void invalidateGetPackagesForUidCache() {
-        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_PACKAGES_FOR_UID_PROPERTY);
+        sGetPackagesForUidCache.invalidateCache();
     }
 
     @Override
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index dce15b8..3633b4e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -11370,7 +11370,7 @@
             if (mProgressPoints == null) {
                 mProgressPoints = new ArrayList<>();
             }
-            if (point.getPosition() >= 0) {
+            if (point.getPosition() > 0) {
                 mProgressPoints.add(point);
 
                 if (mProgressPoints.size() > MAX_PROGRESS_POINT_LIMIT) {
@@ -11379,7 +11379,7 @@
                 }
 
             } else {
-                Log.w(TAG, "Dropped the point. The position is a negative integer.");
+                Log.w(TAG, "Dropped the point. The position is a negative or zero integer.");
             }
 
             return this;
@@ -11893,7 +11893,9 @@
                 final List<Point> points = new ArrayList<>();
                 for (Point point : mProgressPoints) {
                     final int position = point.getPosition();
-                    if (position < 0 || position > totalLength) continue;
+                    // The points at start/end aren't supposed to show in the progress bar.
+                    // Therefore those are also dropped here.
+                    if (position <= 0 || position >= totalLength) continue;
                     points.add(sanitizePoint(point, backgroundColor, defaultProgressColor));
                     if (points.size() == MAX_PROGRESS_POINT_LIMIT) {
                         break;
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index bfb33f2b..c573161 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1163,6 +1163,17 @@
     }
 
     /**
+     * Return the current cache nonce.
+     * @hide
+     */
+    @VisibleForTesting
+    public long getNonce() {
+        synchronized (mLock) {
+            return mNonce.getNonce();
+        }
+    }
+
+    /**
      * Complete key prefixes.
      */
     private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + ".";
@@ -1314,7 +1325,7 @@
 
     /**
      * Burst a property name into module and api.  Throw if the key is invalid.  This method is
-     * used in to transition legacy cache constructors to the args constructor.
+     * used to transition legacy cache constructors to the Args constructor.
      */
     private static Args argsFromProperty(@NonNull String name) {
         throwIfInvalidCacheKey(name);
@@ -1327,6 +1338,15 @@
     }
 
     /**
+     * Return the API porting of a legacy property.  This method is used to transition caches to
+     * the Args constructor.
+     * @hide
+     */
+    public static String apiFromProperty(@NonNull String name) {
+        return argsFromProperty(name).mApi;
+    }
+
+    /**
      * Make a new property invalidated cache.  This constructor names the cache after the
      * property name.  New clients should prefer the constructor that takes an explicit
      * cache name.
@@ -2036,11 +2056,11 @@
     }
 
     /**
-     * Disable all caches in the local process.  This is primarily useful for testing when
-     * the test needs to bypass the cache or when the test is for a server, and the test
-     * process does not have privileges to write SystemProperties. Once disabled it is not
-     * possible to re-enable caching in the current process.  If a client wants to
-     * temporarily disable caching, use the corking mechanism.
+     * Disable all caches in the local process.  This is primarily useful for testing when the
+     * test needs to bypass the cache or when the test is for a server, and the test process does
+     * not have privileges to write the nonce. Once disabled it is not possible to re-enable
+     * caching in the current process.  See {@link #testPropertyName} for a more focused way to
+     * bypass caches when the test is for a server.
      * @hide
      */
     public static void disableForTestMode() {
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 0a3891f..a66d59b 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
 import android.app.appsearch.AppSearchManager;
 import android.content.Context;
 import android.os.CancellationSignal;
@@ -325,8 +326,28 @@
             return;
         }
 
+        // Wrap the callback to convert AppFunctionNotFoundException to IllegalArgumentException
+        // to match the documentation.
+        OutcomeReceiver<Boolean, Exception> callbackWithExceptionInterceptor =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(@NonNull Boolean result) {
+                        callback.onResult(result);
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception exception) {
+                        if (exception instanceof AppFunctionNotFoundException) {
+                            exception = new IllegalArgumentException(exception);
+                        }
+                        callback.onError(exception);
+                    }
+                };
+
         AppFunctionManagerHelper.isAppFunctionEnabled(
-                functionIdentifier, targetPackage, appSearchManager, executor, callback);
+                functionIdentifier, targetPackage, appSearchManager, executor,
+                callbackWithExceptionInterceptor);
+
     }
 
     private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub {
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index cc3ca03..83abc04 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -60,8 +60,8 @@
      * <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.
+     *   <li>{@link AppFunctionNotFoundException}, 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
@@ -216,7 +216,7 @@
     private static @NonNull Exception failedResultToException(
             @NonNull AppSearchResult appSearchResult) {
         return switch (appSearchResult.getResultCode()) {
-            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
+            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException(
                     appSearchResult.getErrorMessage());
             case AppSearchResult.RESULT_IO_ERROR -> new IOException(
                     appSearchResult.getErrorMessage());
@@ -225,4 +225,15 @@
             default -> new IllegalStateException(appSearchResult.getErrorMessage());
         };
     }
+
+    /**
+     * Throws when the app function is not found.
+     *
+     * @hide
+     */
+    public static class AppFunctionNotFoundException extends RuntimeException {
+        private AppFunctionNotFoundException(@NonNull String errorMessage) {
+            super(errorMessage);
+        }
+    }
 }
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 566e78a..2b0e941 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -277,6 +277,23 @@
      */
     public static final int MESSAGE_ONEWAY_TO_WEARABLE = 0x43847987; // +TOW
 
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TRANSPORT_FLAG_" }, value = {
+            TRANSPORT_FLAG_EXTEND_PATCH_DIFF,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TransportFlags {}
+
+    /**
+     * A security flag that allows transports to be attached to devices that may be more vulnerable
+     * due to infrequent updates. Can only be used for associations with
+     * {@link AssociationRequest#DEVICE_PROFILE_WEARABLE_SENSING} device profile.
+     *
+     * @hide
+     */
+    public static final int TRANSPORT_FLAG_EXTEND_PATCH_DIFF = 1;
+
     /**
      * Callback for applications to receive updates about and the outcome of
      * {@link AssociationRequest} issued via {@code associate()} call.
@@ -1452,7 +1469,52 @@
             }
 
             try {
-                final Transport transport = new Transport(associationId, in, out);
+                final Transport transport = new Transport(associationId, in, out, 0);
+                mTransports.put(associationId, transport);
+                transport.start();
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to attach transport", e);
+            }
+        }
+    }
+
+    /**
+     * Attach a bidirectional communication stream to be used as a transport channel for
+     * transporting system data between associated devices. Flags can be provided to further
+     * customize the behavior of the transport.
+     *
+     * @param associationId id of the associated device.
+     * @param in Already connected stream of data incoming from remote
+     *           associated device.
+     * @param out Already connected stream of data outgoing to remote associated
+     *            device.
+     * @param flags Flags to customize transport behavior.
+     * @throws DeviceNotAssociatedException Thrown if the associationId was not previously
+     * associated with this app.
+     *
+     * @see #buildPermissionTransferUserConsentIntent(int)
+     * @see #startSystemDataTransfer(int, Executor, OutcomeReceiver)
+     * @see #detachSystemDataTransport(int)
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
+    public void attachSystemDataTransport(int associationId,
+            @NonNull InputStream in,
+            @NonNull OutputStream out,
+            @TransportFlags int flags) throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
+        synchronized (mTransports) {
+            if (mTransports.contains(associationId)) {
+                detachSystemDataTransport(associationId);
+            }
+
+            try {
+                final Transport transport = new Transport(associationId, in, out, flags);
                 mTransports.put(associationId, transport);
                 transport.start();
             } catch (IOException e) {
@@ -1931,16 +1993,22 @@
         private final int mAssociationId;
         private final InputStream mRemoteIn;
         private final OutputStream mRemoteOut;
+        private final int mFlags;
 
         private InputStream mLocalIn;
         private OutputStream mLocalOut;
 
         private volatile boolean mStopped;
 
-        public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) {
+        Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) {
+            this(associationId, remoteIn, remoteOut, 0);
+        }
+
+        Transport(int associationId, InputStream remoteIn, OutputStream remoteOut, int flags) {
             mAssociationId = associationId;
             mRemoteIn = remoteIn;
             mRemoteOut = remoteOut;
+            mFlags = flags;
         }
 
         public void start() throws IOException {
@@ -1957,7 +2025,7 @@
 
             try {
                 mService.attachSystemDataTransport(mContext.getOpPackageName(),
-                        mContext.getUserId(), mAssociationId, remoteFd);
+                        mContext.getUserId(), mAssociationId, remoteFd, mFlags);
             } catch (RemoteException e) {
                 throw new IOException("Failed to configure transport", e);
             }
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index a2b7dd9..787e8b6 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -113,7 +113,7 @@
         in ISystemDataTransferCallback callback);
 
     @EnforcePermission("DELIVER_COMPANION_MESSAGES")
-    void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd);
+    void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd, int flags);
 
     @EnforcePermission("DELIVER_COMPANION_MESSAGES")
     void detachSystemDataTransport(String packageName, int userId, int associationId);
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 37f3f17..e645060 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1643,6 +1643,19 @@
     public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L;
 
     /**
+     * When the override is enabled, the activity receives configuration coupled with caption bar
+     * insets. Normally, caption bar insets are decoupled from configuration.
+     *
+     * <p>Override applies only if the activity targets SDK level 34 or earlier version.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS = 388014743L;
+
+    /**
      * Optional set of a certificates identifying apps that are allowed to embed this activity. From
      * the "knownActivityEmbeddingCerts" attribute.
      */
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 0369b7d9..6ae2df2 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -16,6 +16,7 @@
 
 package android.content.pm;
 
+import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
 import static android.content.pm.SigningInfo.AppSigningSchemeVersion;
 import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY;
 
@@ -11659,11 +11660,22 @@
         }
     }
 
-    private static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
-            sApplicationInfoCache =
-            new PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>(
-                    2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
-                    "getApplicationInfo") {
+    private static String packageInfoApi() {
+        return PropertyInvalidatedCache.apiFromProperty(
+            PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE);
+    }
+
+    // The maximum number of entries to keep in the packageInfo and applicationInfo caches.
+    private final static int MAX_INFO_CACHE_ENTRIES = 2048;
+
+    /** @hide */
+    @VisibleForTesting
+    public static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
+            sApplicationInfoCache = new PropertyInvalidatedCache<>(
+                new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+                .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+                "getApplicationInfo", null) {
+
                 @Override
                 public ApplicationInfo recompute(ApplicationInfoQuery query) {
                     return getApplicationInfoAsUserUncached(
@@ -11749,10 +11761,11 @@
     }
 
     private static final PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>
-            sPackageInfoCache =
-            new PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>(
-                    2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
-                    "getPackageInfo") {
+            sPackageInfoCache = new PropertyInvalidatedCache<>(
+                new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+                .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+                "getPackageInfo", null) {
+
                 @Override
                 public PackageInfo recompute(PackageInfoQuery query) {
                     return getPackageInfoAsUserUncached(
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 027eb9d..88fbdad 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -137,4 +137,15 @@
     namespace: "resource_manager"
     description: "flag always meant to be false, for testing resource flagging within cts tests"
     bug: "377974898"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "use_new_aconfig_storage"
+    is_exported: true
+    namespace: "resource_manager"
+    description: "Retrieve flag values from new Aconfig flag storage in AconfigFlags.java"
+    bug: "352348353"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 507f8f4..bcb7ebf 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -67,6 +67,7 @@
 import android.os.HandlerExecutor;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceSpecificException;
@@ -1704,7 +1705,9 @@
             return ICameraService.ROTATION_OVERRIDE_NONE;
         }
 
-        if (context != null) {
+        // Isolated process does not have access to ActivityTaskManager service, which is used
+        // indirectly in `ActivityManager.getAppTasks()`.
+        if (context != null && !Process.isIsolated()) {
             final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
             if (activityManager != null) {
                 for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index fded882..d891916 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -641,6 +641,9 @@
      * is triggered whenever the properties of a {@link android.view.Display}, such as size,
      * state, density are modified.
      *
+     * This event is not triggered for refresh rate changes as they can change very often.
+     * To monitor refresh rate changes, subscribe to {@link EVENT_TYPE_DISPLAY_REFRESH_RATE}.
+     *
      * @see #registerDisplayListener(DisplayListener, Handler, long)
      *
      */
@@ -839,6 +842,9 @@
      * Registers a display listener to receive notifications about when
      * displays are added, removed or changed.
      *
+     * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)}
+     * instead to subscribe for explicit events of interest
+     *
      * @param listener The listener to register.
      * @param handler The handler on which the listener should be invoked, or null
      * if the listener should be invoked on the calling thread's looper.
@@ -847,7 +853,9 @@
      */
     public void registerDisplayListener(DisplayListener listener, Handler handler) {
         registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED
-                | EVENT_TYPE_DISPLAY_CHANGED | EVENT_TYPE_DISPLAY_REMOVED);
+                | EVENT_TYPE_DISPLAY_CHANGED
+                | EVENT_TYPE_DISPLAY_REFRESH_RATE
+                | EVENT_TYPE_DISPLAY_REMOVED);
     }
 
     /**
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index b5715ed..339dbf2 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1766,29 +1766,23 @@
         }
 
         if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_CHANGED) != 0) {
-            // For backward compatibility, a client subscribing to
-            // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and
-            // RR changes
-            baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED
-                    | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
+            baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED;
         }
 
-        if ((eventFlags
-                & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) {
+        if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) {
             baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED;
         }
 
-        if (Flags.displayListenerPerformanceImprovements()) {
-            if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) {
-                baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
-            }
+        if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) {
+            baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
+        }
 
+        if (Flags.displayListenerPerformanceImprovements()) {
             if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_STATE) != 0) {
                 baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE;
             }
         }
 
-
         return baseEventMask;
     }
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 0ead823..49db54d 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -19,7 +19,6 @@
 import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
 import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS;
 import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
-import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
 import static com.android.hardware.input.Flags.keyboardGlyphMap;
 
 import android.Manifest;
@@ -966,9 +965,6 @@
     @Nullable
     public Drawable getKeyboardLayoutPreview(@Nullable KeyboardLayout keyboardLayout, int width,
             int height) {
-        if (!keyboardLayoutPreviewFlag()) {
-            return null;
-        }
         PhysicalKeyLayout keyLayout = new PhysicalKeyLayout(
                 mGlobal.getKeyCharacterMap(keyboardLayout), keyboardLayout);
         return new KeyboardLayoutPreviewDrawable(mContext, keyLayout, width, height);
@@ -1403,9 +1399,6 @@
     @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
     public void registerStickyModifierStateListener(@NonNull Executor executor,
             @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
-        if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
-            return;
-        }
         mGlobal.registerStickyModifierStateListener(executor, listener);
     }
 
@@ -1419,9 +1412,6 @@
     @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
     public void unregisterStickyModifierStateListener(
             @NonNull StickyModifierStateListener listener) {
-        if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
-            return;
-        }
         mGlobal.unregisterStickyModifierStateListener(listener);
     }
 
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index af40188..3d4b885 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -16,15 +16,9 @@
 
 package android.hardware.input;
 
-import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG;
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS;
-import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG;
-import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG;
 import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
-import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
-import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
-import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
 import static com.android.hardware.input.Flags.mouseScrollingAcceleration;
 import static com.android.hardware.input.Flags.mouseReverseVerticalScrolling;
 import static com.android.hardware.input.Flags.mouseSwapPrimaryButton;
@@ -871,21 +865,6 @@
     }
 
     /**
-     * Whether Accessibility bounce keys feature is enabled.
-     *
-     * <p>
-     * Bounce keys’ is an accessibility feature to aid users who have physical disabilities,
-     * that allows the user to configure the device to ignore rapid, repeated keypresses of the
-     * same key.
-     * </p>
-     *
-     * @hide
-     */
-    public static boolean isAccessibilityBounceKeysFeatureEnabled() {
-        return keyboardA11yBounceKeysFlag();
-    }
-
-    /**
      * Whether Accessibility bounce keys is enabled.
      *
      * <p>
@@ -912,11 +891,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG)
     public static int getAccessibilityBounceKeysThreshold(@NonNull Context context) {
-        if (!isAccessibilityBounceKeysFeatureEnabled()) {
-            return 0;
-        }
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS, 0, UserHandle.USER_CURRENT);
     }
@@ -936,13 +911,9 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG)
     @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
     public static void setAccessibilityBounceKeysThreshold(@NonNull Context context,
             int thresholdTimeMillis) {
-        if (!isAccessibilityBounceKeysFeatureEnabled()) {
-            return;
-        }
         if (thresholdTimeMillis < 0
                 || thresholdTimeMillis > MAX_ACCESSIBILITY_BOUNCE_KEYS_THRESHOLD_MILLIS) {
             throw new IllegalArgumentException(
@@ -955,21 +926,6 @@
     }
 
     /**
-     * Whether Accessibility slow keys feature flags is enabled.
-     *
-     * <p>
-     * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that
-     * allows the user to specify the duration for which one must press-and-hold a key before the
-     * system accepts the keypress.
-     * </p>
-     *
-     * @hide
-     */
-    public static boolean isAccessibilitySlowKeysFeatureFlagEnabled() {
-        return keyboardA11ySlowKeysFlag();
-    }
-
-    /**
      * Whether Accessibility slow keys is enabled.
      *
      * <p>
@@ -996,11 +952,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG)
     public static int getAccessibilitySlowKeysThreshold(@NonNull Context context) {
-        if (!isAccessibilitySlowKeysFeatureFlagEnabled()) {
-            return 0;
-        }
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_SLOW_KEYS, 0, UserHandle.USER_CURRENT);
     }
@@ -1020,13 +972,9 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG)
     @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
     public static void setAccessibilitySlowKeysThreshold(@NonNull Context context,
             int thresholdTimeMillis) {
-        if (!isAccessibilitySlowKeysFeatureFlagEnabled()) {
-            return;
-        }
         if (thresholdTimeMillis < 0
                 || thresholdTimeMillis > MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS) {
             throw new IllegalArgumentException(
@@ -1039,23 +987,6 @@
     }
 
     /**
-     * Whether Accessibility sticky keys feature is enabled.
-     *
-     * <p>
-     * 'Sticky keys' is an accessibility feature that assists users who have physical
-     * disabilities or help users reduce repetitive strain injury. It serializes keystrokes
-     * instead of pressing multiple keys at a time, allowing the user to press and release a
-     * modifier key, such as Shift, Ctrl, Alt, or any other modifier key, and have it remain
-     * active until any other key is pressed.
-     * </p>
-     *
-     * @hide
-     */
-    public static boolean isAccessibilityStickyKeysFeatureEnabled() {
-        return keyboardA11yStickyKeysFlag();
-    }
-
-    /**
      * Whether Accessibility sticky keys is enabled.
      *
      * <p>
@@ -1069,11 +1000,7 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG)
     public static boolean isAccessibilityStickyKeysEnabled(@NonNull Context context) {
-        if (!isAccessibilityStickyKeysFeatureEnabled()) {
-            return false;
-        }
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_STICKY_KEYS, 0, UserHandle.USER_CURRENT) != 0;
     }
@@ -1092,13 +1019,9 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG)
     @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
     public static void setAccessibilityStickyKeysEnabled(@NonNull Context context,
             boolean enabled) {
-        if (!isAccessibilityStickyKeysFeatureEnabled()) {
-            return;
-        }
         Settings.Secure.putIntForUser(context.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_STICKY_KEYS, enabled ? 1 : 0,
                 UserHandle.USER_CURRENT);
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 23722ed..6c2ce36 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -8,35 +8,6 @@
 
 flag {
     namespace: "input_native"
-    name: "keyboard_layout_preview_flag"
-    description: "Controls whether a preview will be shown in Settings when selecting a physical keyboard layout"
-    bug: "293579375"
-}
-
-
-flag {
-    namespace: "input_native"
-    name: "keyboard_a11y_sticky_keys_flag"
-    description: "Controls if the sticky keys accessibility feature for physical keyboard is available to the user"
-    bug: "294546335"
-}
-
-flag {
-    namespace: "input_native"
-    name: "keyboard_a11y_bounce_keys_flag"
-    description: "Controls if the bounce keys accessibility feature for physical keyboard is available to the user"
-    bug: "294546335"
-}
-
-flag {
-    namespace: "input_native"
-    name: "keyboard_a11y_slow_keys_flag"
-    description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user"
-    bug: "294546335"
-}
-
-flag {
-    namespace: "input_native"
     name: "keyboard_glyph_map"
     description: "Allows system to provide keyboard specific key drawables and shortcuts via config files"
     bug: "345440920"
@@ -233,3 +204,12 @@
     description: "Key Event Activity Detection"
     bug: "356412905"
 }
+
+flag {
+   name: "enable_backup_and_restore_for_input_gestures"
+   namespace: "input"
+   description: "Adds backup and restore support for custom input gestures"
+   bug: "382184249"
+   is_fixed_read_only: true
+}
+
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index e79b2e7..2604454 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -45,7 +45,8 @@
  * {@link PersistableBundle} subclass.
  */
 @android.ravenwood.annotation.RavenwoodKeepWholeClass
-public class BaseBundle {
+@SuppressWarnings("HiddenSuperclass")
+public class BaseBundle implements Parcel.ClassLoaderProvider {
     /** @hide */
     protected static final String TAG = "Bundle";
     static final boolean DEBUG = false;
@@ -311,8 +312,9 @@
 
     /**
      * Return the ClassLoader currently associated with this Bundle.
+     * @hide
      */
-    ClassLoader getClassLoader() {
+    public ClassLoader getClassLoader() {
         return mClassLoader;
     }
 
@@ -426,6 +428,9 @@
             if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) {
                 Intent.maybeMarkAsMissingCreatorToken(object);
             }
+        } else if (object instanceof Bundle) {
+            Bundle bundle = (Bundle) object;
+            bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
         }
         return (clazz != null) ? clazz.cast(object) : (T) object;
     }
@@ -499,7 +504,7 @@
         int[] numLazyValues = new int[]{0};
         try {
             parcelledData.readArrayMap(map, count, !parcelledByNative,
-                    /* lazy */ ownsParcel, mClassLoader, numLazyValues);
+                    /* lazy */ ownsParcel, this, numLazyValues);
         } catch (BadParcelableException e) {
             if (sShouldDefuse) {
                 Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index a24dc57..c0591e6 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -141,6 +141,8 @@
         STRIPPED.putInt("STRIPPED", 1);
     }
 
+    private boolean isFirstRetrievedFromABundle = false;
+
     /**
      * Constructs a new, empty Bundle.
      */
@@ -382,7 +384,15 @@
         bundle.unparcel();
         mOwnsLazyValues = false;
         bundle.mOwnsLazyValues = false;
-        mMap.putAll(bundle.mMap);
+        int N = bundle.mMap.size();
+        for (int i = 0; i < N; i++) {
+            String key = bundle.mMap.keyAt(i);
+            Object value = bundle.mMap.valueAt(i);
+            if (value instanceof Bundle) {
+                ((Bundle) value).isFirstRetrievedFromABundle = true;
+            }
+            mMap.put(key, value);
+        }
 
         // FD and Binders state is now known if and only if both bundles already knew
         if ((bundle.mFlags & FLAG_HAS_FDS) != 0) {
@@ -592,6 +602,8 @@
         mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
         if (intentClass != null && intentClass.isInstance(value)) {
             setHasIntent(true);
+        } else if (value instanceof Bundle) {
+            ((Bundle) value).isFirstRetrievedFromABundle = true;
         }
     }
 
@@ -793,6 +805,9 @@
      */
     public void putBundle(@Nullable String key, @Nullable Bundle value) {
         unparcel();
+        if (value != null) {
+            value.isFirstRetrievedFromABundle = true;
+        }
         mMap.put(key, value);
     }
 
@@ -1020,7 +1035,9 @@
             return null;
         }
         try {
-            return (Bundle) o;
+            Bundle bundle = (Bundle) o;
+            bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
+            return bundle;
         } catch (ClassCastException e) {
             typeWarning(key, o, "Bundle", e);
             return null;
@@ -1028,6 +1045,21 @@
     }
 
     /**
+     * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a
+     * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it
+     * is retrieved from the container bundle first time though. Once it is accessed outside of its
+     * container, its ClassLoader should no longer be changed by its container anymore.
+     *
+     * @param containerBundle the bundle this bundle is retrieved from.
+     */
+    void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) {
+        if (!isFirstRetrievedFromABundle) {
+            setClassLoader(containerBundle.getClassLoader());
+            isFirstRetrievedFromABundle = true;
+        }
+    }
+
+    /**
      * Returns the value associated with the given key, or {@code null} if
      * no mapping of the desired type exists for the given key or a {@code null}
      * value is explicitly associated with the key.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index e589347..49d3f06 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -46,6 +46,7 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 
 import dalvik.annotation.optimization.CriticalNative;
@@ -4661,7 +4662,7 @@
      * @hide
      */
     @Nullable
-    public Object readLazyValue(@Nullable ClassLoader loader) {
+    private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) {
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
@@ -4672,12 +4673,17 @@
             int end = MathUtils.addOrThrow(dataPosition(), objectLength);
             int valueLength = end - start;
             setDataPosition(end);
-            return new LazyValue(this, start, valueLength, type, loader);
+            return new LazyValue(this, start, valueLength, type, loaderProvider);
         } else {
-            return readValue(type, loader, /* clazz */ null);
+            return readValue(type, getClassLoader(loaderProvider), /* clazz */ null);
         }
     }
 
+    @Nullable
+    private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) {
+        return loaderProvider == null ? null : loaderProvider.getClassLoader();
+    }
+
 
     private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> {
         /**
@@ -4691,7 +4697,12 @@
         private final int mPosition;
         private final int mLength;
         private final int mType;
-        @Nullable private final ClassLoader mLoader;
+        // this member is set when a bundle that includes a LazyValue is unparceled. But it is used
+        // when apply method is called. Between these 2 events, the bundle's ClassLoader could have
+        // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current
+        // ClassLoader at the time apply method is called.
+        @NonNull
+        private final ClassLoaderProvider mLoaderProvider;
         @Nullable private Object mObject;
 
         /**
@@ -4702,12 +4713,13 @@
          */
         @Nullable private volatile Parcel mSource;
 
-        LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) {
+        LazyValue(Parcel source, int position, int length, int type,
+                @NonNull ClassLoaderProvider loaderProvider) {
             mSource = requireNonNull(source);
             mPosition = position;
             mLength = length;
             mType = type;
-            mLoader = loader;
+            mLoaderProvider = loaderProvider;
         }
 
         @Override
@@ -4720,7 +4732,8 @@
                         int restore = source.dataPosition();
                         try {
                             source.setDataPosition(mPosition);
-                            mObject = source.readValue(mLoader, clazz, itemTypes);
+                            mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz,
+                                    itemTypes);
                         } finally {
                             source.setDataPosition(restore);
                         }
@@ -4758,6 +4771,12 @@
             return Parcel.hasFileDescriptors(mObject);
         }
 
+        /** @hide */
+        @VisibleForTesting
+        public ClassLoader getClassLoader() {
+            return mLoaderProvider.getClassLoader();
+        }
+
         @Override
         public String toString() {
             return (mSource != null)
@@ -4793,7 +4812,8 @@
                 return Objects.equals(mObject, value.mObject);
             }
             // Better safely fail here since this could mean we get different objects.
-            if (!Objects.equals(mLoader, value.mLoader)) {
+            if (!Objects.equals(mLoaderProvider.getClassLoader(),
+                    value.mLoaderProvider.getClassLoader())) {
                 return false;
             }
             // Otherwise compare metadata prior to comparing payload.
@@ -4807,10 +4827,24 @@
         @Override
         public int hashCode() {
             // Accessing mSource first to provide memory barrier for mObject
-            return Objects.hash(mSource == null, mObject, mLoader, mType, mLength);
+            return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType,
+                    mLength);
         }
     }
 
+    /**
+     * Provides a ClassLoader.
+     * @hide
+     */
+    public interface ClassLoaderProvider {
+        /**
+         * Returns a ClassLoader.
+         *
+         * @return ClassLoader
+         */
+        ClassLoader getClassLoader();
+    }
+
     /** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */
     private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
         // Avoids allocating Class[0] array
@@ -5551,8 +5585,8 @@
     }
 
     private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
-            int size, @Nullable ClassLoader loader) {
-        readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null);
+            int size, @Nullable ClassLoaderProvider loaderProvider) {
+        readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null);
     }
 
     /**
@@ -5566,11 +5600,12 @@
      * @hide
      */
     void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
-            boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) {
+            boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) {
         ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size);
         while (size > 0) {
             String key = readString();
-            Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
+            Object value = (lazy) ? readLazyValue(loaderProvider) : readValue(
+                    getClassLoader(loaderProvider));
             if (value instanceof LazyValue) {
                 lazyValueCount[0]++;
             }
@@ -5591,12 +5626,12 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
-            @Nullable ClassLoader loader) {
+            @Nullable ClassLoaderProvider loaderProvider) {
         final int N = readInt();
         if (N < 0) {
             return;
         }
-        readArrayMapInternal(outVal, N, loader);
+        readArrayMapInternal(outVal, N, loaderProvider);
     }
 
     /**
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 2d9d025..1a54f4d 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -159,7 +159,7 @@
      */
     public void execute(Message message) {
         checkReleased();
-        if (Looper.myLooper() == mLooper) {
+        if (mLooper.isCurrentThread()) {
             // This is being called from the thread it should be executed on, we can just dispatch.
             message.target.dispatchMessage(message);
         } else {
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index 9d0e221..a8a22f6 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -473,17 +473,31 @@
             }
         }
 
-        final HealthStats[] results = new HealthStats[uids.length];
-        if (result.bundle != null) {
-            HealthStatsParceler[] parcelers = result.bundle.getParcelableArray(
-                    IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class);
-            if (parcelers != null && parcelers.length == uids.length) {
-                for (int i = 0; i < parcelers.length; i++) {
-                    results[i] = parcelers[i].getHealthStats();
+        switch (result.resultCode) {
+            case IBatteryStats.RESULT_OK: {
+                final HealthStats[] results = new HealthStats[uids.length];
+                if (result.bundle != null) {
+                    HealthStatsParceler[] parcelers = result.bundle.getParcelableArray(
+                            IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class);
+                    if (parcelers != null && parcelers.length == uids.length) {
+                        for (int i = 0; i < parcelers.length; i++) {
+                            results[i] = parcelers[i].getHealthStats();
+                        }
+                    }
                 }
+                return results;
             }
+            case IBatteryStats.RESULT_SECURITY_EXCEPTION: {
+                throw new SecurityException(result.bundle != null
+                        ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null);
+            }
+            case IBatteryStats.RESULT_RUNTIME_EXCEPTION: {
+                throw new RuntimeException(result.bundle != null
+                        ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null);
+            }
+            default:
+                throw new RuntimeException("Error code: " + result.resultCode);
         }
-        return results;
     }
 
     /**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 91ad22f..24f8672 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -22,6 +22,7 @@
 import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OP_READ_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OP_READ_MEDIA_IMAGES;
+import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
 import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.UserHandle.PER_USER_RANGE;
@@ -44,6 +45,7 @@
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.PendingIntent;
+import android.app.PropertyInvalidatedCache;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -269,14 +271,15 @@
     public static final int FLAG_STORAGE_SDK = IInstalld.FLAG_STORAGE_SDK;
 
     /** {@hide} */
-    @IntDef(prefix = "FLAG_STORAGE_",  value = {
+    @IntDef(prefix = "FLAG_STORAGE_", value = {
             FLAG_STORAGE_DE,
             FLAG_STORAGE_CE,
             FLAG_STORAGE_EXTERNAL,
             FLAG_STORAGE_SDK,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface StorageFlags {}
+    public @interface StorageFlags {
+    }
 
     /** {@hide} */
     public static final int FLAG_FOR_WRITE = 1 << 8;
@@ -309,6 +312,44 @@
     @GuardedBy("mDelegates")
     private final ArrayList<StorageEventListenerDelegate> mDelegates = new ArrayList<>();
 
+    static record VolumeListQuery(int mUserId, String mPackageName, int mFlags) {
+    }
+
+    private static final PropertyInvalidatedCache.QueryHandler<VolumeListQuery, StorageVolume[]>
+            sVolumeListQuery = new PropertyInvalidatedCache.QueryHandler<>() {
+                @androidx.annotation.Nullable
+                @Override
+                public StorageVolume[] apply(@androidx.annotation.NonNull VolumeListQuery query) {
+                    final IStorageManager storageManager = IStorageManager.Stub.asInterface(
+                            ServiceManager.getService("mount"));
+                    if (storageManager == null) {
+                        // negative results won't be cached, so we will just try again next time
+                        return null;
+                    }
+                    try {
+                        return storageManager.getVolumeList(
+                                query.mUserId, query.mPackageName, query.mFlags);
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                }
+            };
+
+    // Generally, the userId and packageName parameters stay pretty constant, but flags may change
+    // regularly; we have observed some processes hitting 10+ variations.
+    private static final int VOLUME_LIST_CACHE_MAX = 16;
+
+    private static final PropertyInvalidatedCache<VolumeListQuery, StorageVolume[]>
+            sVolumeListCache = new PropertyInvalidatedCache<>(
+                    new PropertyInvalidatedCache.Args(MODULE_SYSTEM).cacheNulls(false)
+                    .api("getVolumeList").maxEntries(VOLUME_LIST_CACHE_MAX), "getVolumeList",
+                    sVolumeListQuery);
+
+    /** {@hide} */
+    public static void invalidateVolumeListCache() {
+        sVolumeListCache.invalidateCache();
+    }
+
     private class StorageEventListenerDelegate extends IStorageEventListener.Stub {
         final Executor mExecutor;
         final StorageEventListener mListener;
@@ -395,7 +436,8 @@
 
     private class ObbActionListener extends IObbActionListener.Stub {
         @SuppressWarnings("hiding")
-        private SparseArray<ObbListenerDelegate> mListeners = new SparseArray<ObbListenerDelegate>();
+        private SparseArray<ObbListenerDelegate> mListeners =
+                new SparseArray<ObbListenerDelegate>();
 
         @Override
         public void onObbResult(String filename, int nonce, int status) {
@@ -477,10 +519,10 @@
      *
      * @param looper The {@link android.os.Looper} which events will be received on.
      *
-     * <p>Applications can get instance of this class by calling
-     * {@link android.content.Context#getSystemService(java.lang.String)} with an argument
-     * of {@link android.content.Context#STORAGE_SERVICE}.
-     *
+     *               <p>Applications can get instance of this class by calling
+     *               {@link android.content.Context#getSystemService(java.lang.String)} with an
+     *               argument
+     *               of {@link android.content.Context#STORAGE_SERVICE}.
      * @hide
      */
     @UnsupportedAppUsage
@@ -488,15 +530,16 @@
         mContext = context;
         mResolver = context.getContentResolver();
         mLooper = looper;
-        mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount"));
+        mStorageManager = IStorageManager.Stub.asInterface(
+                ServiceManager.getServiceOrThrow("mount"));
         mAppOps = mContext.getSystemService(AppOpsManager.class);
     }
 
     /**
      * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}.
      *
-     * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object.
-     *
+     * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener}
+     *                 object.
      * @hide
      */
     @UnsupportedAppUsage
@@ -516,14 +559,14 @@
     /**
      * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}.
      *
-     * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object.
-     *
+     * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener}
+     *                 object.
      * @hide
      */
     @UnsupportedAppUsage
     public void unregisterListener(StorageEventListener listener) {
         synchronized (mDelegates) {
-            for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) {
+            for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) {
                 final StorageEventListenerDelegate delegate = i.next();
                 if (delegate.mListener == listener) {
                     try {
@@ -558,7 +601,8 @@
          * {@link StorageManager#getStorageVolumes()} to observe the latest
          * value.
          */
-        public void onStateChanged(@NonNull StorageVolume volume) { }
+        public void onStateChanged(@NonNull StorageVolume volume) {
+        }
     }
 
     /**
@@ -592,7 +636,7 @@
      */
     public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) {
         synchronized (mDelegates) {
-            for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) {
+            for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) {
                 final StorageEventListenerDelegate delegate = i.next();
                 if (delegate.mCallback == callback) {
                     try {
@@ -628,8 +672,8 @@
 
     /**
      * Query if a USB Mass Storage (UMS) host is connected.
-     * @return true if UMS host is connected.
      *
+     * @return true if UMS host is connected.
      * @hide
      */
     @Deprecated
@@ -640,8 +684,8 @@
 
     /**
      * Query if a USB Mass Storage (UMS) is enabled on the device.
-     * @return true if UMS host is enabled.
      *
+     * @return true if UMS host is enabled.
      * @hide
      */
     @Deprecated
@@ -663,11 +707,11 @@
      * That is, shared UID applications can attempt to mount any other
      * application's OBB that shares its UID.
      *
-     * @param rawPath the path to the OBB file
-     * @param key must be <code>null</code>. Previously, some Android device
-     *            implementations accepted a non-<code>null</code> key to mount
-     *            an encrypted OBB file. However, this never worked reliably and
-     *            is no longer supported.
+     * @param rawPath  the path to the OBB file
+     * @param key      must be <code>null</code>. Previously, some Android device
+     *                 implementations accepted a non-<code>null</code> key to mount
+     *                 an encrypted OBB file. However, this never worked reliably and
+     *                 is no longer supported.
      * @param listener will receive the success or failure of the operation
      * @return whether the mount call was successfully queued or not
      */
@@ -739,9 +783,9 @@
      * application's OBB that shares its UID.
      * <p>
      *
-     * @param rawPath path to the OBB file
-     * @param force whether to kill any programs using this in order to unmount
-     *            it
+     * @param rawPath  path to the OBB file
+     * @param force    whether to kill any programs using this in order to unmount
+     *                 it
      * @param listener will receive the success or failure of the operation
      * @return whether the unmount call was successfully queued or not
      */
@@ -781,7 +825,7 @@
      *
      * @param rawPath path to OBB image
      * @return absolute path to mounted OBB image data or <code>null</code> if
-     *         not mounted or exception encountered trying to read status
+     * not mounted or exception encountered trying to read status
      */
     public String getMountedObbPath(String rawPath) {
         Preconditions.checkNotNull(rawPath, "rawPath cannot be null");
@@ -899,7 +943,7 @@
      * {@link #UUID_DEFAULT}.
      *
      * @throws IOException when the storage device hosting the given path isn't
-     *             present, or when it doesn't have a valid UUID.
+     *                     present, or when it doesn't have a valid UUID.
      */
     public @NonNull UUID getUuidForPath(@NonNull File path) throws IOException {
         Preconditions.checkNotNull(path);
@@ -1172,8 +1216,8 @@
     /**
      * This is not the API you're looking for.
      *
-     * @see PackageManager#getPrimaryStorageCurrentVolume()
      * @hide
+     * @see PackageManager#getPrimaryStorageCurrentVolume()
      */
     public String getPrimaryStorageUuid() {
         try {
@@ -1186,8 +1230,8 @@
     /**
      * This is not the API you're looking for.
      *
-     * @see PackageManager#movePrimaryStorage(VolumeInfo)
      * @hide
+     * @see PackageManager#movePrimaryStorage(VolumeInfo)
      */
     public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {
         try {
@@ -1216,7 +1260,7 @@
         // resolve the actual volume name
         if (Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL)) {
             try (Cursor c = mContext.getContentResolver().query(uri,
-                    new String[] { MediaStore.MediaColumns.VOLUME_NAME }, null, null)) {
+                    new String[]{MediaStore.MediaColumns.VOLUME_NAME}, null, null)) {
                 if (c.moveToFirst()) {
                     volumeName = c.getString(0);
                 }
@@ -1275,6 +1319,7 @@
 
     /**
      * Gets the state of a volume via its mountpoint.
+     *
      * @hide
      */
     @Deprecated
@@ -1308,7 +1353,7 @@
      * Return the list of shared/external storage volumes currently available to
      * the calling user and the user it shares media with. Please refer to
      * <a href="https://source.android.com/compatibility/12/android-12-cdd#95_multi-user_support">
-     *     multi-user support</a> for more details.
+     * multi-user support</a> for more details.
      *
      * <p>
      * This is similar to {@link StorageManager#getStorageVolumes()} except that the result also
@@ -1353,7 +1398,7 @@
     public static Pair<String, Long> getPrimaryStoragePathAndSize() {
         return Pair.create(null,
                 FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()
-                    + Environment.getRootDirectory().getTotalSpace()));
+                        + Environment.getRootDirectory().getTotalSpace()));
     }
 
     /** {@hide} */
@@ -1389,8 +1434,6 @@
     /** {@hide} */
     @UnsupportedAppUsage
     public static @NonNull StorageVolume[] getVolumeList(int userId, int flags) {
-        final IStorageManager storageManager = IStorageManager.Stub.asInterface(
-                ServiceManager.getService("mount"));
         try {
             String packageName = ActivityThread.currentOpPackageName();
             if (packageName == null) {
@@ -1406,7 +1449,7 @@
                 }
                 packageName = packageNames[0];
             }
-            return storageManager.getVolumeList(userId, packageName, flags);
+            return sVolumeListCache.query(new VolumeListQuery(userId, packageName, flags));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1414,6 +1457,7 @@
 
     /**
      * Returns list of paths for all mountable volumes.
+     *
      * @hide
      */
     @Deprecated
@@ -1605,7 +1649,7 @@
      * <p>
      * This is only intended to be called by UserManagerService, as part of creating a user.
      *
-     * @param userId ID of the user
+     * @param userId    ID of the user
      * @param ephemeral whether the user is ephemeral
      * @throws RuntimeException on error.  The user's keys already existing is considered an error.
      * @hide
@@ -1711,7 +1755,8 @@
         return false;
     }
 
-    /** {@hide}
+    /**
+     * {@hide}
      * Is this device encrypted?
      * <p>
      * Note: all devices launching with Android 10 (API level 29) or later are
@@ -1724,8 +1769,10 @@
         return RoSystemProperties.CRYPTO_ENCRYPTED;
     }
 
-    /** {@hide}
+    /**
+     * {@hide}
      * Does this device have file-based encryption (FBE) enabled?
+     *
      * @return true if the device has file-based encryption enabled.
      */
     public static boolean isFileEncrypted() {
@@ -1759,8 +1806,8 @@
     }
 
     /**
-     * @deprecated disabled now that FUSE has been replaced by sdcardfs
      * @hide
+     * @deprecated disabled now that FUSE has been replaced by sdcardfs
      */
     @Deprecated
     public static File maybeTranslateEmulatedPathToInternal(File path) {
@@ -1790,6 +1837,7 @@
 
     /**
      * Check that given app holds both permission and appop.
+     *
      * @hide
      */
     public static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid,
@@ -1800,6 +1848,7 @@
 
     /**
      * Check that given app holds both permission and appop but do not noteOp.
+     *
      * @hide
      */
     public static boolean checkPermissionAndCheckOp(Context context, boolean enforce,
@@ -1810,6 +1859,7 @@
 
     /**
      * Check that given app holds both permission and appop.
+     *
      * @hide
      */
     private static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid,
@@ -1877,7 +1927,9 @@
                 // Legacy apps technically have the access granted by this op,
                 // even when the op is denied
                 if ((mAppOps.checkOpNoThrow(OP_LEGACY_STORAGE, uid,
-                        packageName) == AppOpsManager.MODE_ALLOWED)) return true;
+                        packageName) == AppOpsManager.MODE_ALLOWED)) {
+                    return true;
+                }
 
                 if (enforce) {
                     throw new SecurityException("Op " + AppOpsManager.opToName(op) + " "
@@ -1924,7 +1976,7 @@
             return true;
         }
         if (mode == AppOpsManager.MODE_DEFAULT && mContext.checkPermission(
-                  MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) {
+                MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) {
             return true;
         }
         // If app doesn't have MANAGE_EXTERNAL_STORAGE, then check if it has requested granular
@@ -1936,7 +1988,7 @@
     @VisibleForTesting
     public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
             int mode, ProxyFileDescriptorCallback callback, Handler handler, ThreadFactory factory)
-                    throws IOException {
+            throws IOException {
         Preconditions.checkNotNull(callback);
         MetricsLogger.count(mContext, "storage_open_proxy_file_descriptor", 1);
         // Retry is needed because the mount point mFuseAppLoop is using may be unmounted before
@@ -1987,7 +2039,7 @@
     /** {@hide} */
     public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
             int mode, ProxyFileDescriptorCallback callback)
-                    throws IOException {
+            throws IOException {
         return openProxyFileDescriptor(mode, callback, null, null);
     }
 
@@ -2006,19 +2058,18 @@
      * you're willing to decrypt on-demand, but where you want to avoid
      * persisting the cleartext version.
      *
-     * @param mode The desired access mode, must be one of
-     *            {@link ParcelFileDescriptor#MODE_READ_ONLY},
-     *            {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or
-     *            {@link ParcelFileDescriptor#MODE_READ_WRITE}
+     * @param mode     The desired access mode, must be one of
+     *                 {@link ParcelFileDescriptor#MODE_READ_ONLY},
+     *                 {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or
+     *                 {@link ParcelFileDescriptor#MODE_READ_WRITE}
      * @param callback Callback to process file operation requests issued on
-     *            returned file descriptor.
-     * @param handler Handler that invokes callback methods.
+     *                 returned file descriptor.
+     * @param handler  Handler that invokes callback methods.
      * @return Seekable ParcelFileDescriptor.
-     * @throws IOException
      */
     public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
             int mode, ProxyFileDescriptorCallback callback, Handler handler)
-                    throws IOException {
+            throws IOException {
         Preconditions.checkNotNull(handler);
         return openProxyFileDescriptor(mode, callback, handler, null);
     }
@@ -2050,10 +2101,10 @@
      * </p>
      *
      * @param storageUuid the UUID of the storage volume that you're interested
-     *            in. The UUID for a specific path can be obtained using
-     *            {@link #getUuidForPath(File)}.
+     *                    in. The UUID for a specific path can be obtained using
+     *                    {@link #getUuidForPath(File)}.
      * @throws IOException when the storage device isn't present, or when it
-     *             doesn't support cache quotas.
+     *                     doesn't support cache quotas.
      * @see #getCacheSizeBytes(UUID)
      */
     @WorkerThread
@@ -2085,10 +2136,10 @@
      * </p>
      *
      * @param storageUuid the UUID of the storage volume that you're interested
-     *            in. The UUID for a specific path can be obtained using
-     *            {@link #getUuidForPath(File)}.
+     *                    in. The UUID for a specific path can be obtained using
+     *                    {@link #getUuidForPath(File)}.
      * @throws IOException when the storage device isn't present, or when it
-     *             doesn't support cache quotas.
+     *                     doesn't support cache quotas.
      * @see #getCacheQuotaBytes(UUID)
      */
     @WorkerThread
@@ -2106,7 +2157,7 @@
 
 
     /** @hide */
-    @IntDef(prefix = { "MOUNT_MODE_" }, value = {
+    @IntDef(prefix = {"MOUNT_MODE_"}, value = {
             MOUNT_MODE_EXTERNAL_NONE,
             MOUNT_MODE_EXTERNAL_DEFAULT,
             MOUNT_MODE_EXTERNAL_INSTALLER,
@@ -2115,16 +2166,19 @@
     })
     @Retention(RetentionPolicy.SOURCE)
     /** @hide */
-    public @interface MountMode {}
+    public @interface MountMode {
+    }
 
     /**
      * No external storage should be mounted.
+     *
      * @hide
      */
     @SystemApi
     public static final int MOUNT_MODE_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE;
     /**
      * Default external storage should be mounted.
+     *
      * @hide
      */
     @SystemApi
@@ -2132,12 +2186,14 @@
     /**
      * Mount mode for package installers which should give them access to
      * all obb dirs in addition to their package sandboxes
+     *
      * @hide
      */
     @SystemApi
     public static final int MOUNT_MODE_EXTERNAL_INSTALLER = IVold.REMOUNT_MODE_INSTALLER;
     /**
      * The lower file system should be bind mounted directly on external storage
+     *
      * @hide
      */
     @SystemApi
@@ -2146,6 +2202,7 @@
     /**
      * Use the regular scoped storage filesystem, but Android/ should be writable.
      * Used to support the applications hosting DownloadManager and the MTP server.
+     *
      * @hide
      */
     @SystemApi
@@ -2164,10 +2221,10 @@
      * this flag to take effect.
      * </p>
      *
+     * @hide
      * @see #getAllocatableBytes(UUID, int)
      * @see #allocateBytes(UUID, long, int)
      * @see #allocateBytes(FileDescriptor, long, int)
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE)
     @SystemApi
@@ -2194,6 +2251,7 @@
      * freeable cached space when determining allocatable space.
      *
      * Intended for use with {@link #getAllocatableBytes()}.
+     *
      * @hide
      */
     public static final int FLAG_ALLOCATE_NON_CACHE_ONLY = 1 << 3;
@@ -2203,12 +2261,13 @@
      * cached space when determining allocatable space.
      *
      * Intended for use with {@link #getAllocatableBytes()}.
+     *
      * @hide
      */
     public static final int FLAG_ALLOCATE_CACHE_ONLY = 1 << 4;
 
     /** @hide */
-    @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = {
+    @IntDef(flag = true, prefix = {"FLAG_ALLOCATE_"}, value = {
             FLAG_ALLOCATE_AGGRESSIVE,
             FLAG_ALLOCATE_DEFY_ALL_RESERVED,
             FLAG_ALLOCATE_DEFY_HALF_RESERVED,
@@ -2216,7 +2275,8 @@
             FLAG_ALLOCATE_CACHE_ONLY,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface AllocateFlags {}
+    public @interface AllocateFlags {
+    }
 
     /**
      * Return the maximum number of new bytes that your app can allocate for
@@ -2246,15 +2306,15 @@
      * </p>
      *
      * @param storageUuid the UUID of the storage volume where you're
-     *            considering allocating disk space, since allocatable space can
-     *            vary widely depending on the underlying storage device. The
-     *            UUID for a specific path can be obtained using
-     *            {@link #getUuidForPath(File)}.
+     *                    considering allocating disk space, since allocatable space can
+     *                    vary widely depending on the underlying storage device. The
+     *                    UUID for a specific path can be obtained using
+     *                    {@link #getUuidForPath(File)}.
      * @return the maximum number of new bytes that the calling app can allocate
-     *         using {@link #allocateBytes(UUID, long)} or
-     *         {@link #allocateBytes(FileDescriptor, long)}.
+     * using {@link #allocateBytes(UUID, long)} or
+     * {@link #allocateBytes(FileDescriptor, long)}.
      * @throws IOException when the storage device isn't present, or when it
-     *             doesn't support allocating space.
+     *                     doesn't support allocating space.
      */
     @WorkerThread
     public @BytesLong long getAllocatableBytes(@NonNull UUID storageUuid)
@@ -2297,12 +2357,12 @@
      * more than once every 60 seconds.
      *
      * @param storageUuid the UUID of the storage volume where you'd like to
-     *            allocate disk space. The UUID for a specific path can be
-     *            obtained using {@link #getUuidForPath(File)}.
-     * @param bytes the number of bytes to allocate.
+     *                    allocate disk space. The UUID for a specific path can be
+     *                    obtained using {@link #getUuidForPath(File)}.
+     * @param bytes       the number of bytes to allocate.
      * @throws IOException when the storage device isn't present, or when it
-     *             doesn't support allocating space, or if the device had
-     *             trouble allocating the requested space.
+     *                     doesn't support allocating space, or if the device had
+     *                     trouble allocating the requested space.
      * @see #getAllocatableBytes(UUID)
      */
     @WorkerThread
@@ -2332,10 +2392,9 @@
      * These mount modes specify different views and access levels for
      * different apps on external storage.
      *
+     * @return {@code MountMode} for the given uid and packageName.
      * @params uid UID of the application
      * @params packageName name of the package
-     * @return {@code MountMode} for the given uid and packageName.
-     *
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE)
@@ -2366,15 +2425,15 @@
      * (such as when recording a video) you should avoid calling this method
      * more than once every 60 seconds.
      *
-     * @param fd the open file that you'd like to allocate disk space for.
+     * @param fd    the open file that you'd like to allocate disk space for.
      * @param bytes the number of bytes to allocate. This is the desired final
-     *            size of the open file. If the open file is smaller than this
-     *            requested size, it will be extended without modifying any
-     *            existing contents. If the open file is larger than this
-     *            requested size, it will be truncated.
+     *              size of the open file. If the open file is smaller than this
+     *              requested size, it will be extended without modifying any
+     *              existing contents. If the open file is larger than this
+     *              requested size, it will be truncated.
      * @throws IOException when the storage device isn't present, or when it
-     *             doesn't support allocating space, or if the device had
-     *             trouble allocating the requested space.
+     *                     doesn't support allocating space, or if the device had
+     *                     trouble allocating the requested space.
      * @see #isAllocationSupported(FileDescriptor)
      * @see Environment#isExternalStorageEmulated(File)
      */
@@ -2499,13 +2558,14 @@
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = { "QUOTA_TYPE_" }, value = {
+    @IntDef(prefix = {"QUOTA_TYPE_"}, value = {
             QUOTA_TYPE_MEDIA_NONE,
             QUOTA_TYPE_MEDIA_AUDIO,
             QUOTA_TYPE_MEDIA_VIDEO,
             QUOTA_TYPE_MEDIA_IMAGE,
     })
-    public @interface QuotaType {}
+    public @interface QuotaType {
+    }
 
     private static native boolean setQuotaProjectId(String path, long projectId);
 
@@ -2532,15 +2592,13 @@
      * The default platform user of this API is the MediaProvider process, which is
      * responsible for managing all of external storage.
      *
-     * @param path the path to the file for which we should update the quota type
+     * @param path      the path to the file for which we should update the quota type
      * @param quotaType the quota type of the file; this is based on the
      *                  {@code QuotaType} constants, eg
      *                  {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO}
-     *
      * @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid
      *                                  quota type.
      * @throws IOException              if the quota type could not be updated.
-     *
      * @hide
      */
     @SystemApi
@@ -2616,7 +2674,6 @@
      * permissions of a directory to what they should anyway be.
      *
      * @param path the path for which we should fix up the permissions
-     *
      * @hide
      */
     public void fixupAppDir(@NonNull File path) {
@@ -2822,11 +2879,12 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = { "APP_IO_BLOCKED_REASON_" }, value = {
-                APP_IO_BLOCKED_REASON_TRANSCODING,
-                APP_IO_BLOCKED_REASON_UNKNOWN,
+    @IntDef(prefix = {"APP_IO_BLOCKED_REASON_"}, value = {
+            APP_IO_BLOCKED_REASON_TRANSCODING,
+            APP_IO_BLOCKED_REASON_UNKNOWN,
     })
-    public @interface AppIoBlockedReason {}
+    public @interface AppIoBlockedReason {
+    }
 
     /**
      * Notify the system that an app with {@code uid} and {@code tid} is blocked on an IO request on
@@ -2839,10 +2897,9 @@
      * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
      *
      * @param volumeUuid the UUID of the storage volume that the app IO is blocked on
-     * @param uid the UID of the app blocked on IO
-     * @param tid the tid of the app blocked on IO
-     * @param reason the reason the app is blocked on IO
-     *
+     * @param uid        the UID of the app blocked on IO
+     * @param tid        the tid of the app blocked on IO
+     * @param reason     the reason the app is blocked on IO
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2866,10 +2923,9 @@
      * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
      *
      * @param volumeUuid the UUID of the storage volume that the app IO is resumed on
-     * @param uid the UID of the app resuming IO
-     * @param tid the tid of the app resuming IO
-     * @param reason the reason the app is resuming IO
-     *
+     * @param uid        the UID of the app resuming IO
+     * @param tid        the tid of the app resuming IO
+     * @param reason     the reason the app is resuming IO
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2890,10 +2946,9 @@
      * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
      *
      * @param volumeUuid the UUID of the storage volume to check IO blocked status
-     * @param uid the UID of the app to check IO blocked status
-     * @param tid the tid of the app to check IO blocked status
-     * @param reason the reason to check IO blocked status for
-     *
+     * @param uid        the UID of the app to check IO blocked status
+     * @param tid        the tid of the app to check IO blocked status
+     * @param reason     the reason to check IO blocked status for
      * @hide
      */
     @TestApi
@@ -2962,7 +3017,6 @@
      * information is available, -1 is returned.
      *
      * @return Percentage of the remaining useful lifetime of the internal storage device.
-     *
      * @hide
      */
     @FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API)
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 5b527c7..1b65a88 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -1124,12 +1124,13 @@
         }
 
         final Uri documentUri = extraUri;
-        final String authority = documentUri.getAuthority();
+        final String authorityWithoutUserId = getAuthorityWithoutUserId(documentUri.getAuthority());
         final String documentId = DocumentsContract.getDocumentId(documentUri);
 
-        if (!mAuthority.equals(authority)) {
+        if (!mAuthority.equals(authorityWithoutUserId)) {
             throw new SecurityException(
-                    "Requested authority " + authority + " doesn't match provider " + mAuthority);
+                    "Requested authority " + authorityWithoutUserId + " doesn't match provider "
+                            + mAuthority);
         }
 
         if (METHOD_IS_CHILD_DOCUMENT.equals(method)) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 3cd7a00..f1a9514 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13006,6 +13006,24 @@
         public static final String STYLUS_POINTER_ICON_ENABLED = "stylus_pointer_icon_enabled";
 
         /**
+         * Toggle for whether to redact OTP notification while connected to wifi. Defaults to
+         * false/0.
+         * @hide
+         */
+        @Readable
+        public static final String REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI =
+                "redact_otp_on_wifi";
+
+        /**
+         * Toggle for whether to immediately redact OTP notifications, or require the device to be
+         * locked for 10 minutes. Defaults to false/0
+         * @hide
+         */
+        @Readable
+        public static final String REDACT_OTP_NOTIFICATION_IMMEDIATELY =
+                "remove_otp_redaction_delay";
+
+        /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
          */
diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java
index 195896d..0e78bfd 100644
--- a/core/java/android/view/InputEventConsistencyVerifier.java
+++ b/core/java/android/view/InputEventConsistencyVerifier.java
@@ -180,7 +180,7 @@
             final MotionEvent motionEvent = (MotionEvent)event;
             if (motionEvent.isTouchEvent()) {
                 onTouchEvent(motionEvent, nestingLevel);
-            } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+            } else if (motionEvent.isFromSource(InputDevice.SOURCE_TRACKBALL)) {
                 onTrackballEvent(motionEvent, nestingLevel);
             } else {
                 onGenericMotionEvent(motionEvent, nestingLevel);
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index 1c36eaf..9c1f134 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -290,9 +290,15 @@
     @SuppressWarnings("unused")
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private void dispatchInputEvent(int seq, InputEvent event) {
-        Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event));
+        if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) {
+            // This 'if' block is an optimization - without it, 'getShortDescription' will be
+            // called unconditionally, which is expensive.
+            Trace.traceBegin(Trace.TRACE_TAG_INPUT,
+                    "dispatchInputEvent " + getShortDescription(event));
+        }
         mSeqMap.put(event.getSequenceNumber(), seq);
         onInputEvent(event);
+        // If tracing is not enabled, `traceEnd` is a no-op (so we don't need to guard it with 'if')
         Trace.traceEnd(Trace.TRACE_TAG_INPUT);
     }
 
diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java
index 5e1eada..331e345 100644
--- a/core/java/android/view/RoundScrollbarRenderer.java
+++ b/core/java/android/view/RoundScrollbarRenderer.java
@@ -20,6 +20,7 @@
 
 import static java.lang.Math.sin;
 
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
@@ -40,9 +41,9 @@
 
     // The range of the scrollbar position represented as an angle in degrees.
     private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
-    private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90%
-    private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 3.1f; // 10%
-    private static final float THUMB_WIDTH_DP = 4f;
+    private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 0.7f * SCROLLBAR_ANGLE_RANGE;
+    private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE;
+    private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f;
     private static final float OUTER_PADDING_DP = 2f;
     private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF;
     private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
@@ -57,14 +58,16 @@
     private final RectF mRect = new RectF();
     private final View mParent;
     private final float mInset;
+    private final float mGapBetweenThumbAndTrackPx;
+    private final boolean mUseRefactoredRoundScrollbar;
 
     private float mPreviousMaxScroll = 0;
     private float mMaxScrollDiff = 0;
     private float mPreviousCurrentScroll = 0;
     private float mCurrentScrollDiff = 0;
     private float mThumbStrokeWidthAsDegrees = 0;
+    private float mGapBetweenTrackAndThumbAsDegrees = 0;
     private boolean mDrawToLeft;
-    private boolean mUseRefactoredRoundScrollbar;
 
     public RoundScrollbarRenderer(View parent) {
         // Paints for the round scrollbar.
@@ -80,16 +83,17 @@
 
         mParent = parent;
 
+        Resources resources = parent.getContext().getResources();
         // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
         // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
         // that it doesn't get clipped.
         int maskThickness =
-                parent.getContext()
-                        .getResources()
-                        .getDimensionPixelSize(
-                                com.android.internal.R.dimen.circular_display_mask_thickness);
+                resources.getDimensionPixelSize(
+                        com.android.internal.R.dimen.circular_display_mask_thickness);
 
-        float thumbWidth = dpToPx(THUMB_WIDTH_DP);
+        float thumbWidth =
+                resources.getDimensionPixelSize(com.android.internal.R.dimen.round_scrollbar_width);
+        mGapBetweenThumbAndTrackPx = dpToPx(GAP_BETWEEN_TRACK_AND_THUMB_DP);
         mThumbPaint.setStrokeWidth(thumbWidth);
         mTrackPaint.setStrokeWidth(thumbWidth);
         mInset = thumbWidth / 2 + maskThickness;
@@ -175,7 +179,6 @@
         }
     }
 
-    /** Returns true if horizontal bounds are updated */
     private void updateBounds(Rect bounds) {
         mRect.set(
                 bounds.left + mInset,
@@ -184,6 +187,8 @@
                 bounds.bottom - mInset);
         mThumbStrokeWidthAsDegrees =
                 getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f);
+        mGapBetweenTrackAndThumbAsDegrees =
+                getVertexAngle((mRect.right - mRect.left) / 2f, mGapBetweenThumbAndTrackPx);
     }
 
     private float computeSweepAngle(float scrollExtent, float maxScroll) {
@@ -262,20 +267,22 @@
                 // The highest point of the top track on a vertical scale. Here the thumb width is
                 // reduced to account for the arc formed by ROUND stroke style
                 -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees,
-                // The lowest point of the top track on a vertical scale. Here the thumb width is
-                // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap
-                // between thumb and top track
-                thumbStartAngle - mThumbStrokeWidthAsDegrees * 2,
+                // The lowest point of the top track on a vertical scale. It's reduced by
+                // (a) angular distance for the arc formed by ROUND stroke style
+                // (b) gap between thumb and top track
+                thumbStartAngle - mThumbStrokeWidthAsDegrees - mGapBetweenTrackAndThumbAsDegrees,
                 alpha);
         // Draws the thumb
         drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint);
         // Draws the bottom arc
         drawTrack(
                 canvas,
-                // The highest point of the bottom track on a vertical scale. Here the thumb width
-                // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap
-                // between thumb and bottom track
-                (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2,
+                // The highest point of the bottom track on a vertical scale. Following added to it
+                // (a) angular distance for the arc formed by ROUND stroke style
+                // (b) gap between thumb and top track
+                (thumbStartAngle + thumbSweepAngle)
+                        + mThumbStrokeWidthAsDegrees
+                        + mGapBetweenTrackAndThumbAsDegrees,
                 // The lowest point of the top track on a vertical scale. Here the thumb width is
                 // added to account for the arc formed by ROUND stroke style
                 SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees,
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 900f22d..80b4f2c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -133,6 +133,7 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
+import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange;
 import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
 
@@ -272,6 +273,8 @@
 import android.window.ScreenCapture;
 import android.window.SurfaceSyncGroup;
 import android.window.WindowOnBackInvokedDispatcher;
+import android.window.WindowTokenClient;
+import android.window.WindowTokenClientController;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -6609,7 +6612,19 @@
             mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId,
                     activityWindowInfo);
         } else {
-            // There is no activity callback - update the configuration right away.
+            if (enableWindowContextResourcesUpdateOnConfigChange()) {
+                // There is no activity callback - update resources for window token, if needed.
+                final IBinder windowContextToken = mContext.getWindowContextToken();
+                if (windowContextToken instanceof WindowTokenClient) {
+                    WindowTokenClientController.getInstance().onWindowConfigurationChanged(
+                            windowContextToken,
+                            mLastReportedMergedConfiguration.getMergedConfiguration(),
+                            newDisplayId == INVALID_DISPLAY
+                                    ? mDisplay.getDisplayId()
+                                    : newDisplayId
+                    );
+                }
+            }
             updateConfiguration(newDisplayId);
         }
         mForceNextConfigUpdate = false;
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index edfa1d5..db699d7 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1531,15 +1531,6 @@
      */
     @TestApi
     static boolean hasWindowExtensionsEnabled() {
-        if (!Flags.enableWmExtensionsForAllFlag() && ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) {
-            // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
-            // on all devices by default as a build file property.
-            // Until finishing flag ramp up, only return true when
-            // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
-            // OEMs.
-            return false;
-        }
-
         if (!HAS_WINDOW_EXTENSIONS_ON_DEVICE) {
             return false;
         }
diff --git a/core/java/android/view/accessibility/OWNERS b/core/java/android/view/accessibility/OWNERS
index f62b33f..799ef00 100644
--- a/core/java/android/view/accessibility/OWNERS
+++ b/core/java/android/view/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 # Android Accessibility Framework owners
 include /services/accessibility/OWNERS
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 2fb78c0..b66020b 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -57,6 +57,7 @@
 import android.view.ViewStructure;
 import android.view.autofill.AutofillId;
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import android.view.contentcapture.flags.Flags;
 import android.view.contentprotection.ContentProtectionEventProcessor;
 import android.view.inputmethod.BaseInputConnection;
 
@@ -1008,6 +1009,9 @@
                     }
                 }
                 internalNotifyViewTreeEvent(sessionId, /* started= */ false);
+                if (Flags.flushAfterEachFrame()) {
+                    internalNotifySessionFlushEvent(sessionId);
+                }
             }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
diff --git a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig
index e7bc004..8c98fa4 100644
--- a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig
+++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig
@@ -15,3 +15,14 @@
     bug: "380381249"
     is_exported: true
 }
+
+flag {
+    name: "flush_after_each_frame"
+    namespace: "pixel_state_server"
+    description: "Feature flag to send a flush event after each frame"
+    bug: "380381249"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0fb8042..56f0415 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -3778,8 +3778,32 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             if (Flags.refactorInsetsController()) {
-                mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(),
-                        false /* fromIme */, statsToken);
+                synchronized (mH) {
+                    Handler vh = rootView.getHandler();
+                    if (vh == null) {
+                        // If the view doesn't have a handler, something has changed out from
+                        // under us.
+                        ImeTracker.forLogging().onFailed(statsToken,
+                                ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
+                        return;
+                    }
+                    ImeTracker.forLogging().onProgress(statsToken,
+                        ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
+
+                    if (vh.getLooper() != Looper.myLooper()) {
+                        // The view is running on a different thread than our own, so
+                        // we need to reschedule our work for over there.
+                        if (DEBUG) {
+                            Log.v(TAG, "Close current input: reschedule hide to view thread");
+                        }
+                        final var viewRootImpl = mCurRootView;
+                        vh.post(() -> viewRootImpl.getInsetsController().hide(
+                                WindowInsets.Type.ime(), false /* fromIme */, statsToken));
+                    } else {
+                        mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(),
+                                false /* fromIme */, statsToken);
+                    }
+                }
             } else {
                 IInputMethodManagerGlobalInvoker.hideSoftInput(
                         mClient,
diff --git a/core/java/android/window/ConfigurationDispatcher.java b/core/java/android/window/ConfigurationDispatcher.java
new file mode 100644
index 0000000..b8f0da1
--- /dev/null
+++ b/core/java/android/window/ConfigurationDispatcher.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.window;
+
+import android.annotation.NonNull;
+import android.content.ComponentCallbacks;
+import android.content.res.Configuration;
+
+/**
+ * Indicates a {@link android.content.Context} could propagate the
+ * {@link android.content.res.Configuration} from the server side and users may listen to the
+ * updates through {@link android.content.Context#registerComponentCallbacks(ComponentCallbacks)}.
+ *
+ * @hide
+ */
+public interface ConfigurationDispatcher {
+
+    /**
+     * Called when there's configuration update from the server side.
+     */
+    void dispatchConfigurationChanged(@NonNull Configuration configuration);
+
+    /**
+     * Indicates that if this dispatcher should report the change even if it's not
+     * {@link Configuration#diffPublicOnly}.
+     */
+    default boolean shouldReportPrivateChanges() {
+        return false;
+    }
+}
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 7852460..1ce5df7 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -55,6 +55,7 @@
             Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
     ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix,
             true),
+    ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false),
     ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true),
     ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
             Flags::enableDesktopRecentsTransitionsCornersBugfix, false),
diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java
index 89327fe..bc5ad50 100644
--- a/core/java/android/window/TaskFragmentCreationParams.java
+++ b/core/java/android/window/TaskFragmentCreationParams.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.TestApi;
+import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityInfo.ScreenOrientation;
 import android.graphics.Rect;
 import android.os.IBinder;
@@ -112,12 +113,21 @@
      */
     private final @ScreenOrientation int mOverrideOrientation;
 
+    /**
+     * {@link android.content.pm.ActivityInfo.Config} mask that specifies which
+     * configuration changes should trigger TaskFragment info change callbacks.
+     *
+     * @see android.content.pm.ActivityInfo.Config
+     */
+    private final @ActivityInfo.Config int mConfigurationChangeMask;
+
     private TaskFragmentCreationParams(
             @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken,
             @NonNull IBinder ownerToken, @NonNull Rect initialRelativeBounds,
             @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken,
             @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty,
-            @ScreenOrientation int overrideOrientation) {
+            @ScreenOrientation int overrideOrientation,
+            @ActivityInfo.Config int configurationChangeMask) {
         if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) {
             throw new IllegalArgumentException("pairedPrimaryFragmentToken and"
                     + " pairedActivityToken should not be set at the same time.");
@@ -131,6 +141,7 @@
         mPairedActivityToken = pairedActivityToken;
         mAllowTransitionWhenEmpty = allowTransitionWhenEmpty;
         mOverrideOrientation = overrideOrientation;
+        mConfigurationChangeMask = configurationChangeMask;
     }
 
     @NonNull
@@ -186,6 +197,11 @@
         return mOverrideOrientation;
     }
 
+    /** @hide */
+    public @ActivityInfo.Config int getConfigurationChangeMask() {
+        return mConfigurationChangeMask;
+    }
+
     private TaskFragmentCreationParams(Parcel in) {
         mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
         mFragmentToken = in.readStrongBinder();
@@ -196,6 +212,7 @@
         mPairedActivityToken = in.readStrongBinder();
         mAllowTransitionWhenEmpty = in.readBoolean();
         mOverrideOrientation = in.readInt();
+        mConfigurationChangeMask = in.readInt();
     }
 
     /** @hide */
@@ -210,6 +227,7 @@
         dest.writeStrongBinder(mPairedActivityToken);
         dest.writeBoolean(mAllowTransitionWhenEmpty);
         dest.writeInt(mOverrideOrientation);
+        dest.writeInt(mConfigurationChangeMask);
     }
 
     @NonNull
@@ -238,6 +256,7 @@
                 + " pairedActivityToken=" + mPairedActivityToken
                 + " allowTransitionWhenEmpty=" + mAllowTransitionWhenEmpty
                 + " overrideOrientation=" + mOverrideOrientation
+                + " configurationChangeMask=" + mConfigurationChangeMask
                 + "}";
     }
 
@@ -275,6 +294,8 @@
 
         private @ScreenOrientation int mOverrideOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
 
+        private @ActivityInfo.Config int mConfigurationChangeMask = 0;
+
         public Builder(@NonNull TaskFragmentOrganizerToken organizer,
                 @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
             mOrganizer = organizer;
@@ -369,12 +390,30 @@
             return this;
         }
 
+        /**
+         * Sets {@link android.content.pm.ActivityInfo.Config} mask that specifies which
+         * configuration changes should trigger TaskFragment info change callbacks.
+         *
+         * Only system organizers are allowed to configure this value. This value is ignored for
+         * non-system organizers.
+         *
+         * @see android.content.pm.ActivityInfo.Config
+         * @hide
+         */
+        @NonNull
+        public Builder setConfigurationChangeMask(
+                @ActivityInfo.Config int configurationChangeMask) {
+            mConfigurationChangeMask = configurationChangeMask;
+            return this;
+        }
+
         /** Constructs the options to create TaskFragment with. */
         @NonNull
         public TaskFragmentCreationParams build() {
             return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken,
                     mInitialRelativeBounds, mWindowingMode, mPairedPrimaryFragmentToken,
-                    mPairedActivityToken, mAllowTransitionWhenEmpty, mOverrideOrientation);
+                    mPairedActivityToken, mAllowTransitionWhenEmpty, mOverrideOrientation,
+                    mConfigurationChangeMask);
         }
     }
 }
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 5121136..cf21e50 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -1291,12 +1291,13 @@
             return options;
         }
 
-        /** Make options for a scale-up animation. */
+        /** Make options for a scale-up animation with task override option */
         @NonNull
         public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width,
-                int height) {
+                int height, boolean overrideTaskTransition) {
             AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP);
             options.mTransitionBounds.set(startX, startY, startX + width, startY + height);
+            options.mOverrideTaskTransition = overrideTaskTransition;
             return options;
         }
 
diff --git a/core/java/android/window/WindowContext.java b/core/java/android/window/WindowContext.java
index 84a8b8f..778ccaf 100644
--- a/core/java/android/window/WindowContext.java
+++ b/core/java/android/window/WindowContext.java
@@ -17,8 +17,6 @@
 
 import static android.view.WindowManagerImpl.createWindowContextWindowManager;
 
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UiContext;
@@ -46,7 +44,8 @@
  * @hide
  */
 @UiContext
-public class WindowContext extends ContextWrapper implements WindowProvider {
+public class WindowContext extends ContextWrapper implements WindowProvider,
+        ConfigurationDispatcher {
     private final WindowManager mWindowManager;
     @WindowManager.LayoutParams.WindowType
     private final int mType;
@@ -155,7 +154,7 @@
     }
 
     /** Dispatch {@link Configuration} to each {@link ComponentCallbacks}. */
-    @VisibleForTesting(visibility = PACKAGE)
+    @Override
     public void dispatchConfigurationChanged(@NonNull Configuration newConfig) {
         mCallbacksController.dispatchConfigurationChanged(newConfig);
     }
@@ -170,4 +169,10 @@
     public Bundle getWindowContextOptions() {
         return mOptions;
     }
+
+    @Override
+    public boolean shouldReportPrivateChanges() {
+        // Always dispatch config changes to WindowContext.
+        return true;
+    }
 }
diff --git a/core/java/android/window/WindowContextController.java b/core/java/android/window/WindowContextController.java
index 1e2f454..d31e43f 100644
--- a/core/java/android/window/WindowContextController.java
+++ b/core/java/android/window/WindowContextController.java
@@ -86,7 +86,6 @@
      * @param token The token used to attach to a window manager node. It is usually from
      *              {@link Context#getWindowContextToken()}.
      */
-    @VisibleForTesting
     public WindowContextController(@NonNull WindowTokenClient token) {
         mToken = token;
     }
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index c81c9ec..8468867 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -49,9 +49,11 @@
  *
  * @hide
  */
+@SuppressWarnings("HiddenSuperclass")
 @TestApi
 @UiContext
-public abstract class WindowProviderService extends Service implements WindowProvider {
+public abstract class WindowProviderService extends Service implements WindowProvider,
+        ConfigurationDispatcher {
 
     private static final String TAG = WindowProviderService.class.getSimpleName();
 
@@ -240,4 +242,14 @@
         mController.detachIfNeeded();
         mCallbacksController.clearCallbacks();
     }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    public void dispatchConfigurationChanged(@NonNull Configuration configuration) {
+        onConfigurationChanged(configuration);
+    }
 }
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index a551fe7..9b296c8 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -106,8 +106,8 @@
      * @param newConfig the updated {@link Configuration}
      * @param newDisplayId the updated {@link android.view.Display} ID
      */
-    @VisibleForTesting(visibility = PACKAGE)
     @MainThread
+    @VisibleForTesting(visibility = PACKAGE)
     public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
         onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */);
     }
@@ -121,8 +121,6 @@
                 newDisplayId, true /* shouldReportConfigChange */).recycleOnUse());
     }
 
-    // TODO(b/192048581): rewrite this method based on WindowContext and WindowProviderService
-    //  are inherited from WindowProvider.
     /**
      * Called when {@link Configuration} updates from the server side receive.
      *
@@ -169,7 +167,7 @@
         CompatibilityInfo.applyOverrideIfNeeded(newConfig);
         final boolean displayChanged;
         final boolean shouldUpdateResources;
-        final int diff;
+        final int publicDiff;
         final Configuration currentConfig;
 
         synchronized (mConfiguration) {
@@ -177,7 +175,7 @@
             shouldUpdateResources = shouldUpdateResources(this, mConfiguration,
                     newConfig, newConfig /* overrideConfig */, displayChanged,
                     null /* configChanged */);
-            diff = mConfiguration.diffPublicOnly(newConfig);
+            publicDiff = mConfiguration.diffPublicOnly(newConfig);
             currentConfig = mShouldDumpConfigForIme ? new Configuration(mConfiguration) : null;
             if (shouldUpdateResources) {
                 mConfiguration.setTo(newConfig);
@@ -200,17 +198,15 @@
             // TODO(ag/9789103): update resource manager logic to track non-activity tokens
             mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId);
 
-            if (shouldReportConfigChange && context instanceof WindowContext) {
-                final WindowContext windowContext = (WindowContext) context;
-                windowContext.dispatchConfigurationChanged(newConfig);
+            if (shouldReportConfigChange && context instanceof ConfigurationDispatcher dispatcher) {
+                // Updating resources implies some fields of configuration are updated despite they
+                // are public or not.
+                if (dispatcher.shouldReportPrivateChanges() || publicDiff != 0) {
+                    dispatcher.dispatchConfigurationChanged(newConfig);
+                }
             }
 
-            if (shouldReportConfigChange && diff != 0
-                    && context instanceof WindowProviderService) {
-                final WindowProviderService windowProviderService = (WindowProviderService) context;
-                windowProviderService.onConfigurationChanged(newConfig);
-            }
-            freeTextLayoutCachesIfNeeded(diff);
+            freeTextLayoutCachesIfNeeded(publicDiff);
             if (mShouldDumpConfigForIme) {
                 if (!shouldReportConfigChange) {
                     Log.d(TAG, "Only apply configuration update to Resources because "
@@ -219,7 +215,7 @@
                             + ", config=" + context.getResources().getConfiguration()
                             + ", display ID=" + context.getDisplayId() + "\n"
                             + Debug.getCallers(5));
-                } else if (diff == 0) {
+                } else if (publicDiff == 0) {
                     Log.d(TAG, "Configuration not dispatch to IME because configuration has no "
                             + " public difference with updated config. "
                             + " Current config=" + context.getResources().getConfiguration()
diff --git a/core/java/android/window/WindowTokenClientController.java b/core/java/android/window/WindowTokenClientController.java
index fcd7dfb..72278d9 100644
--- a/core/java/android/window/WindowTokenClientController.java
+++ b/core/java/android/window/WindowTokenClientController.java
@@ -25,7 +25,9 @@
 import android.app.servertransaction.WindowContextInfoChangeItem;
 import android.app.servertransaction.WindowContextWindowRemovalItem;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArraySet;
@@ -50,6 +52,7 @@
     private final Object mLock = new Object();
     private final IApplicationThread mAppThread = ActivityThread.currentActivityThread()
             .getApplicationThread();
+    private final Handler mHandler = ActivityThread.currentActivityThread().getHandler();
 
     /** Attached {@link WindowTokenClient}. */
     @GuardedBy("mLock")
@@ -257,6 +260,20 @@
         }
     }
 
+    /** Propagates the configuration change to the client token. */
+    public void onWindowConfigurationChanged(@NonNull IBinder clientToken,
+            @NonNull Configuration config, int displayId) {
+        final WindowTokenClient windowTokenClient = getWindowTokenClientIfAttached(clientToken);
+        if (windowTokenClient != null) {
+            // Let's make sure it's called on the main thread!
+            if (mHandler.getLooper().isCurrentThread()) {
+                windowTokenClient.onConfigurationChanged(config, displayId);
+            } else {
+                windowTokenClient.postOnConfigurationChanged(config, displayId);
+            }
+        }
+    }
+
     @Nullable
     private WindowTokenClient getWindowTokenClientIfAttached(@NonNull IBinder clientToken) {
         if (!(clientToken instanceof WindowTokenClient windowTokenClient)) {
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d413ba0..b805ac5 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -572,6 +572,13 @@
 }
 
 flag {
+    name: "enable_display_reconnect_interaction"
+    namespace: "lse_desktop_experience"
+    description: "Enables new interaction that occurs when a display is reconnected."
+    bug: "365873835"
+}
+
+flag {
     name: "show_desktop_experience_dev_option"
     namespace: "lse_desktop_experience"
     description: "Replace the freeform windowing dev options with a desktop experience one."
@@ -670,6 +677,17 @@
 }
 
 flag {
+    name: "enable_window_context_resources_update_on_config_change"
+    namespace: "lse_desktop_experience"
+    description: "Updates window context resources before the view receives the config change callback."
+    bug: "394527409"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_desktop_tab_tearing_minimize_animation_bugfix"
     namespace: "lse_desktop_experience"
     description: "Enabling a minimize animation when a new window is opened via tab tearing and the Desktop Windowing open windows limit is reached."
@@ -677,4 +695,14 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+
+flag {
+    name: "enable_desktop_close_shortcut_bugfix"
+    namespace: "lse_desktop_experience"
+    description: "Fix the window-close keyboard shortcut in Desktop Mode."
+    bug: "394599430"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index ac6625b..54d0eef 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -55,14 +55,6 @@
 
 flag {
     namespace: "windowing_sdk"
-    name: "enable_wm_extensions_for_all_flag"
-    description: "Whether to enable WM Extensions for all devices"
-    bug: "306666082"
-    is_fixed_read_only: true
-}
-
-flag {
-    namespace: "windowing_sdk"
     name: "activity_embedding_animation_customization_flag"
     description: "Whether the animation customization feature for AE is enabled"
     bug: "293658614"
diff --git a/core/java/com/android/internal/accessibility/OWNERS b/core/java/com/android/internal/accessibility/OWNERS
index 1265dfa..dac64f4 100644
--- a/core/java/com/android/internal/accessibility/OWNERS
+++ b/core/java/com/android/internal/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 # Android Accessibility Framework owners
 include /services/accessibility/OWNERS
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl
index a5e166b..5f1a641 100644
--- a/core/java/com/android/internal/app/IBatteryStats.aidl
+++ b/core/java/com/android/internal/app/IBatteryStats.aidl
@@ -35,8 +35,20 @@
 
 interface IBatteryStats {
     /** @hide */
+    const int RESULT_OK = 0;
+
+    /** @hide */
+    const int RESULT_RUNTIME_EXCEPTION = 1;
+
+    /** @hide */
+    const int RESULT_SECURITY_EXCEPTION = 2;
+
+    /** @hide */
     const String KEY_UID_SNAPSHOTS = "uid_snapshots";
 
+    /** @hide */
+    const String KEY_EXCEPTION_MESSAGE = "exception";
+
     // These first methods are also called by native code, so must
     // be kept in sync with frameworks/native/libs/binder/include_batterystats/batterystats/IBatteryStats.h
     @EnforcePermission("UPDATE_DEVICE_STATS")
diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS
index 731dca9..df86725 100644
--- a/core/java/com/android/internal/graphics/palette/OWNERS
+++ b/core/java/com/android/internal/graphics/palette/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 484670

-dupin@google.com

-jamesoleary@google.com
\ No newline at end of file
+# Bug component: 484670
+dupin@google.com
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 7c5335c..9085bbe 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -306,8 +306,15 @@
     /** Track work utility view animation shrinking when scrolling down app list. */
     public static final int CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK = 127;
 
+    /**
+     * Track task transitions
+     *
+     * <p>Tracking starts and ends with the animation.</p>
+     */
+    public static final int CUJ_DEFAULT_TASK_TO_TASK_ANIMATION = 128;
+
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
-    @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+    @VisibleForTesting static final int LAST_CUJ = CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
 
     /** @hide */
     @IntDef({
@@ -426,7 +433,8 @@
             CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON,
             CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
             CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND,
-            CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK
+            CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK,
+            CUJ_DEFAULT_TASK_TO_TASK_ANIMATION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {}
@@ -556,6 +564,7 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_EXPAND;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DEFAULT_TASK_TO_TASK_ANIMATION] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DEFAULT_TASK_TO_TASK_ANIMATION;
     }
 
     private Cuj() {
@@ -806,6 +815,8 @@
                 return "LAUNCHER_WORK_UTILITY_VIEW_EXPAND";
             case CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK:
                 return "LAUNCHER_WORK_UTILITY_VIEW_SHRINK";
+            case CUJ_DEFAULT_TASK_TO_TASK_ANIMATION:
+                return "CUJ_DEFAULT_TASK_TO_TASK_ANIMATION";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index e60879e..38dc198 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -429,6 +429,27 @@
                     null /*dependentPackages*/, null /*dependencies*/, false /*isNative*/));
         }
 
+        if (Flags.enableMediaAndLocationPreload()) {
+            // As these libraries are technically optional and not necessarily inherited from
+            // base_system.mk, only cache them if they exist.
+            final String mediaJarPath = "/system/framework/com.android.media.remotedisplay.jar";
+            if (new File(mediaJarPath).exists()) {
+                libs.add(new SharedLibraryInfo(
+                        mediaJarPath, null /*packageName*/,
+                        null /*codePaths*/, null /*name*/, 0 /*version*/,
+                        SharedLibraryInfo.TYPE_BUILTIN, null /*declaringPackage*/,
+                        null /*dependentPackages*/, null /*dependencies*/, false /*isNative*/));
+            }
+            final String locationJarPath = "/system/framework/com.android.location.provider.jar";
+            if (new File(locationJarPath).exists()) {
+                libs.add(new SharedLibraryInfo(
+                        locationJarPath, null /*packageName*/,
+                        null /*codePaths*/, null /*name*/, 0 /*version*/,
+                        SharedLibraryInfo.TYPE_BUILTIN, null /*declaringPackage*/,
+                        null /*dependentPackages*/, null /*dependencies*/, false /*isNative*/));
+            }
+        }
+
         // WindowManager Extensions is an optional shared library that is required for WindowManager
         // Jetpack to fully function. Since it is a widely used library, preload it to improve apps
         // startup performance.
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 25a9fbc..32cde50 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -53,6 +53,13 @@
 }
 
 flag {
+    name: "enable_media_and_location_preload"
+    namespace: "system_performance"
+    description: "Enables zygote preload of non-BCP media and location libraries."
+    bug: "241474956"
+}
+
+flag {
     name: "use_transaction_codes_for_unknown_methods"
     namespace: "stability"
     description: "Use transaction codes when the method names is unknown"
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 445dac7..21d000d 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.pm.pkg.component;
 
+import static android.provider.flags.Flags.newStoragePublicApi;
 import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE;
 
 import android.aconfig.DeviceProtos;
@@ -27,6 +28,7 @@
 import android.content.res.Flags;
 import android.os.Environment;
 import android.os.Process;
+import android.os.flagging.AconfigPackage;
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.util.Xml;
@@ -43,6 +45,7 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * A class that manages a cache of all device feature flags and their default + override values.
@@ -58,7 +61,8 @@
     private static final String OVERRIDE_PREFIX = "device_config_overrides/";
     private static final String STAGED_PREFIX = "staged/";
 
-    private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>();
+    private final Map<String, Boolean> mFlagValues = new ArrayMap<>();
+    private final Map<String, AconfigPackage> mAconfigPackages = new ConcurrentHashMap<>();
 
     public AconfigFlags() {
         if (!Flags.manifestFlagging()) {
@@ -67,21 +71,31 @@
             }
             return;
         }
-        final var defaultFlagProtoFiles =
-                (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths()
-                        : Arrays.asList(DeviceProtos.PATHS);
-        for (String fileName : defaultFlagProtoFiles) {
-            try (var inputStream = new FileInputStream(fileName)) {
-                loadAconfigDefaultValues(inputStream.readAllBytes());
-            } catch (IOException e) {
-                Slog.e(LOG_TAG, "Failed to read Aconfig values from " + fileName, e);
+
+        if (useNewStorage()) {
+            Slog.i(LOG_TAG, "Using new flag storage");
+        } else {
+            Slog.i(LOG_TAG, "Using OLD proto flag storage");
+            final var defaultFlagProtoFiles =
+                    (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths()
+                            : Arrays.asList(DeviceProtos.PATHS);
+            for (String fileName : defaultFlagProtoFiles) {
+                try (var inputStream = new FileInputStream(fileName)) {
+                    loadAconfigDefaultValues(inputStream.readAllBytes());
+                } catch (IOException e) {
+                    Slog.w(LOG_TAG, "Failed to read Aconfig values from " + fileName, e);
+                }
+            }
+            if (Process.myUid() == Process.SYSTEM_UID) {
+                // Server overrides are only accessible to the system, no need to even try loading
+                // them in user processes.
+                loadServerOverrides();
             }
         }
-        if (Process.myUid() == Process.SYSTEM_UID) {
-            // Server overrides are only accessible to the system, no need to even try loading them
-            // in user processes.
-            loadServerOverrides();
-        }
+    }
+
+    private static boolean useNewStorage() {
+        return newStoragePublicApi() && Flags.useNewAconfigStorage();
     }
 
     private void loadServerOverrides() {
@@ -200,7 +214,40 @@
      */
     @Nullable
     public Boolean getFlagValue(@NonNull String flagPackageAndName) {
-        Boolean value = mFlagValues.get(flagPackageAndName);
+        if (useNewStorage()) {
+            return getFlagValueFromNewStorage(flagPackageAndName);
+        } else {
+            Boolean value = mFlagValues.get(flagPackageAndName);
+            if (DEBUG) {
+                Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
+            }
+            return value;
+        }
+    }
+
+    private Boolean getFlagValueFromNewStorage(String flagPackageAndName) {
+        int index = flagPackageAndName.lastIndexOf('.');
+        if (index < 0) {
+            Slog.e(LOG_TAG, "Unable to parse package name from " + flagPackageAndName);
+            return null;
+        }
+        String flagPackage = flagPackageAndName.substring(0, index);
+        String flagName = flagPackageAndName.substring(index + 1);
+        Boolean value = null;
+        AconfigPackage aconfigPackage = mAconfigPackages.computeIfAbsent(flagPackage, p -> {
+            try {
+                return AconfigPackage.load(p);
+            } catch (Exception e) {
+                Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e);
+                return null;
+            }
+        });
+        if (aconfigPackage != null) {
+            // Default value is false for when the flag is not found.
+            // Note: Unlike with the old storage, with AconfigPackage, we don't have a way to
+            // know if the flag is not found or if it's found but the value is false.
+            value = aconfigPackage.getBooleanFlagValue(flagName, false);
+        }
         if (DEBUG) {
             Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
         }
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 270cf08..e20a52b 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -231,6 +231,7 @@
     private int mLastRightInset = 0;
     @UnsupportedAppUsage
     private int mLastLeftInset = 0;
+    private WindowInsets mLastInsets = null;
     private boolean mLastHasTopStableInset = false;
     private boolean mLastHasBottomStableInset = false;
     private boolean mLastHasRightStableInset = false;
@@ -1100,6 +1101,7 @@
             mLastWindowFlags = attrs.flags;
 
             if (insets != null) {
+                mLastInsets = insets;
                 mLastForceConsumingTypes = insets.getForceConsumingTypes();
                 mLastForceConsumingOpaqueCaptionBar = insets.isForceConsumingOpaqueCaptionBar();
 
@@ -1176,6 +1178,7 @@
                     mForceWindowDrawsBarBackgrounds, requestedVisibleTypes);
         }
 
+        int consumingTypes = 0;
         // When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
         // mForceWindowDrawsBarBackgrounds, we still need to ensure that the rest of the view
         // hierarchy doesn't notice it, unless they've explicitly asked for it.
@@ -1186,43 +1189,47 @@
         //
         // Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer
         // consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.
-        boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+        final boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
                 || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0;
-        boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
-        boolean forceConsumingNavBar =
+        final boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
+
+        final boolean fitsNavBar =
+                (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
+                        && decorFitsSystemWindows
+                        && !hideNavigation;
+        final boolean forceConsumingNavBar =
                 ((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled)
                         && (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
-                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
-                        && decorFitsSystemWindows
-                        && !hideNavigation)
+                        && fitsNavBar)
                 || ((mLastForceConsumingTypes & WindowInsets.Type.navigationBars()) != 0
                         && hideNavigation);
-
-        boolean consumingNavBar =
-                ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
-                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
-                        && decorFitsSystemWindows
-                        && !hideNavigation)
+        final boolean consumingNavBar =
+                ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 && fitsNavBar)
                 || forceConsumingNavBar;
+        if (consumingNavBar) {
+            consumingTypes |= WindowInsets.Type.navigationBars();
+        }
 
-        // If we didn't request fullscreen layout, but we still got it because of the
-        // mForceWindowDrawsBarBackgrounds flag, also consume top inset.
+        // If the fullscreen layout was not requested, but still received because of the
+        // mForceWindowDrawsBarBackgrounds flag, also consume status bar.
         // If we should always consume system bars, only consume that if the app wanted to go to
         // fullscreen, as otherwise we can expect the app to handle it.
-        boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
+        final boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
                 || (attrs.flags & FLAG_FULLSCREEN) != 0;
         final boolean hideStatusBar = fullscreen
                 || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0;
-        boolean consumingStatusBar =
-                ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
-                        && decorFitsSystemWindows
-                        && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
-                        && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
-                        && mForceWindowDrawsBarBackgrounds
-                        && mLastTopInset != 0)
+        if (((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+                && decorFitsSystemWindows
+                && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
+                && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
+                && mForceWindowDrawsBarBackgrounds
+                && mLastTopInset != 0)
                 || ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0
-                        && hideStatusBar);
+                        && hideStatusBar)) {
+            consumingTypes |= WindowInsets.Type.statusBars();
+        }
 
+        // Decide if caption bar need to be consumed
         final boolean hideCaptionBar = fullscreen
                 || (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0;
         final boolean consumingCaptionBar =
@@ -1237,22 +1244,23 @@
                         && mLastForceConsumingOpaqueCaptionBar
                         && isOpaqueCaptionBar;
 
-        final int consumedTop =
-                (consumingStatusBar || consumingCaptionBar || consumingOpaqueCaptionBar)
-                        ? mLastTopInset : 0;
-        int consumedRight = consumingNavBar ? mLastRightInset : 0;
-        int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
-        int consumedLeft = consumingNavBar ? mLastLeftInset : 0;
+        if (consumingCaptionBar || consumingOpaqueCaptionBar) {
+            consumingTypes |= WindowInsets.Type.captionBar();
+        }
+
+        final Insets consumedInsets = mLastInsets != null
+                ? mLastInsets.getInsets(consumingTypes) : Insets.NONE;
 
         if (mContentRoot != null
                 && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
             MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
-            if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
-                    || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
-                lp.topMargin = consumedTop;
-                lp.rightMargin = consumedRight;
-                lp.bottomMargin = consumedBottom;
-                lp.leftMargin = consumedLeft;
+            if (lp.topMargin != consumedInsets.top || lp.rightMargin != consumedInsets.right
+                    || lp.bottomMargin != consumedInsets.bottom || lp.leftMargin !=
+                    consumedInsets.left) {
+                lp.topMargin = consumedInsets.top;
+                lp.rightMargin = consumedInsets.right;
+                lp.bottomMargin = consumedInsets.bottom;
+                lp.leftMargin = consumedInsets.left;
                 mContentRoot.setLayoutParams(lp);
 
                 if (insets == null) {
@@ -1261,11 +1269,8 @@
                     requestApplyInsets();
                 }
             }
-            if (insets != null && (consumedLeft > 0
-                    || consumedTop > 0
-                    || consumedRight > 0
-                    || consumedBottom > 0)) {
-                insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
+            if (insets != null && !Insets.NONE.equals(consumedInsets)) {
+                insets = insets.inset(consumedInsets);
             }
         }
 
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 05a33fe..d8cf258 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -160,19 +160,21 @@
         Objects.requireNonNull(mConfigurationService,
                 "A null ProtoLog Configuration Service was provided!");
 
-        try {
-            var args = createConfigurationServiceRegisterClientArgs();
+        mBackgroundLoggingService.execute(() -> {
+            try {
+                var args = createConfigurationServiceRegisterClientArgs();
 
-            final var groupArgs = mLogGroups.values().stream()
-                    .map(group -> new RegisterClientArgs
-                            .GroupConfig(group.name(), group.isLogToLogcat()))
-                    .toArray(RegisterClientArgs.GroupConfig[]::new);
-            args.setGroups(groupArgs);
+                final var groupArgs = mLogGroups.values().stream()
+                        .map(group -> new RegisterClientArgs
+                                .GroupConfig(group.name(), group.isLogToLogcat()))
+                        .toArray(RegisterClientArgs.GroupConfig[]::new);
+                args.setGroups(groupArgs);
 
-            mConfigurationService.registerClient(this, args);
-        } catch (RemoteException e) {
-            throw new RuntimeException("Failed to register ProtoLog client");
-        }
+                mConfigurationService.registerClient(this, args);
+            } catch (RemoteException e) {
+                throw new RuntimeException("Failed to register ProtoLog client");
+            }
+        });
     }
 
     /**
diff --git a/core/java/com/android/internal/protolog/WmProtoLogGroups.java b/core/java/com/android/internal/protolog/WmProtoLogGroups.java
index 4bd5d24..5edc2fb 100644
--- a/core/java/com/android/internal/protolog/WmProtoLogGroups.java
+++ b/core/java/com/android/internal/protolog/WmProtoLogGroups.java
@@ -100,6 +100,8 @@
     WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM),
     WM_DEBUG_EMBEDDED_WINDOWS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM),
+    WM_DEBUG_PRESENTATION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM),
     TEST_GROUP(true, true, false, "WindowManagerProtoLogTest");
 
     private final boolean mEnabled;
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index 5e82772..905d4dd 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -83,7 +83,7 @@
 
     /** @see R.styleable#NotificationProgressBar_trackerHeight */
     private final int mTrackerHeight;
-    private int mTrackerWidth;
+    private int mTrackerDrawWidth = 0;
     private int mTrackerPos;
     private final Matrix mMatrix = new Matrix();
     private Matrix mTrackerDrawMatrix = null;
@@ -157,7 +157,7 @@
         } else {
             // TODO: b/372908709 - maybe don't rerun the entire calculation every time the
             //  progress model is updated? For example, if the segments and parts aren't changed,
-            //  there is no need to call `processAndConvertToViewParts` again.
+            //  there is no need to call `processModelAndConvertToViewParts` again.
 
             final int progress = mProgressModel.getProgress();
             final int progressMax = mProgressModel.getProgressMax();
@@ -286,8 +286,11 @@
     private void configureTrackerBounds() {
         // Reset the tracker draw matrix to null
         mTrackerDrawMatrix = null;
+        mTrackerDrawWidth = 0;
 
-        if (mTracker == null || mTrackerHeight <= 0) {
+        if (mTracker == null) return;
+        if (mTrackerHeight <= 0) {
+            mTrackerDrawWidth = mTracker.getIntrinsicWidth();
             return;
         }
 
@@ -306,14 +309,14 @@
         if (dWidth > maxDWidth) {
             scale = (float) mTrackerHeight / (float) dHeight;
             dx = (maxDWidth * scale - dWidth * scale) * 0.5f;
-            mTrackerWidth = (int) (maxDWidth * scale);
+            mTrackerDrawWidth = (int) (maxDWidth * scale);
         } else if (dHeight > maxDHeight) {
             scale = (float) mTrackerHeight * 0.5f / (float) dWidth;
             dy = (maxDHeight * scale - dHeight * scale) * 0.5f;
-            mTrackerWidth = mTrackerHeight / 2;
+            mTrackerDrawWidth = mTrackerHeight / 2;
         } else {
             scale = (float) mTrackerHeight / (float) dHeight;
-            mTrackerWidth = (int) (dWidth * scale);
+            mTrackerDrawWidth = (int) (dWidth * scale);
         }
 
         mTrackerDrawMatrix.setScale(scale, scale);
@@ -449,7 +452,8 @@
                 segSegGap,
                 segPointGap,
                 pointRadius,
-                mHasTrackerIcon
+                mHasTrackerIcon,
+                mTrackerDrawWidth
         );
 
         final float segmentMinWidth = mNotificationProgressDrawable.getSegmentMinWidth();
@@ -465,7 +469,6 @@
                     segmentMinWidth,
                     pointRadius,
                     progressFraction,
-                    width,
                     isStyledByProgress,
                     progressGap
             );
@@ -493,8 +496,8 @@
                         pointRadius,
                         mHasTrackerIcon,
                         segmentMinWidth,
-                        isStyledByProgress
-                );
+                        isStyledByProgress,
+                        mTrackerDrawWidth);
             } catch (NotEnoughWidthToFitAllPartsException ex) {
                 Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback",
                         ex);
@@ -522,8 +525,8 @@
                         pointRadius,
                         mHasTrackerIcon,
                         segmentMinWidth,
-                        isStyledByProgress
-                );
+                        isStyledByProgress,
+                        mTrackerDrawWidth);
             } catch (NotEnoughWidthToFitAllPartsException ex) {
                 Log.w(TAG,
                         "Failed to stretch and rescale segments with single segments and no points",
@@ -537,16 +540,20 @@
                     mParts,
                     mProgressDrawableParts,
                     progressFraction,
-                    width,
                     isStyledByProgress,
                     progressGap);
         }
 
+        // Extend the first and last segments to fill the entire width.
+        p.first.getFirst().setStart(0);
+        p.first.getLast().setEnd(width);
+
         if (DEBUG) {
             Log.d(TAG, "Updating NotificationProgressDrawable parts");
         }
         mNotificationProgressDrawable.setParts(p.first);
-        mAdjustedProgressFraction = p.second / width;
+        mAdjustedProgressFraction =
+                (p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth);
     }
 
     private void updateTrackerAndBarPos(int w, int h) {
@@ -607,7 +614,7 @@
         int available = w - mPaddingLeft - mPaddingRight;
         final int trackerWidth = tracker.getIntrinsicWidth();
         final int trackerHeight = tracker.getIntrinsicHeight();
-        available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth);
+        available -= mTrackerDrawWidth;
 
         final int trackerPos = (int) (progressFraction * available + 0.5f);
 
@@ -672,7 +679,7 @@
         canvas.translate(mPaddingLeft + mTrackerPos, mPaddingTop);
 
         if (mTrackerHeight > 0) {
-            canvas.clipRect(0, 0, mTrackerWidth, mTrackerHeight);
+            canvas.clipRect(0, 0, mTrackerDrawWidth, mTrackerHeight);
         }
 
         if (mTrackerDrawMatrix != null) {
@@ -751,6 +758,7 @@
             throw new IllegalArgumentException("Invalid progress : " + progress);
         }
 
+
         for (ProgressStyle.Point point : points) {
             final int pos = point.getPosition();
             if (pos < 0 || pos > progressMax) {
@@ -758,6 +766,19 @@
             }
         }
 
+        // There should be no points at start or end. If there are, drop them with a warning.
+        points.removeIf(point -> {
+            final int pos = point.getPosition();
+            if (pos == 0) {
+                Log.w(TAG, "Dropping point at start");
+                return true;
+            } else if (pos == progressMax) {
+                Log.w(TAG, "Dropping point at end");
+                return true;
+            }
+            return false;
+        });
+
         final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap(
                 segments);
         final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
@@ -891,12 +912,14 @@
             float segSegGap,
             float segPointGap,
             float pointRadius,
-            boolean hasTrackerIcon
-    ) {
+            boolean hasTrackerIcon,
+            int trackerDrawWidth) {
         List<DrawablePart> drawableParts = new ArrayList<>();
 
-        // generally, we will start drawing at (x, y) and end at (x+w, y)
-        float x = (float) 0;
+        float available = totalWidth - trackerDrawWidth;
+        // Generally, we will start the first segment at (x+trackerDrawWidth/2, y) and end the last
+        // segment at (x+w-trackerDrawWidth/2, y)
+        float x = trackerDrawWidth / 2F;
 
         final int nParts = parts.size();
         for (int iPart = 0; iPart < nParts; iPart++) {
@@ -904,15 +927,14 @@
             final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1);
             final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1);
             if (part instanceof Segment segment) {
-                final float segWidth = segment.mFraction * totalWidth;
+                final float segWidth = segment.mFraction * available;
                 // Advance the start position to account for a point immediately prior.
-                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap,
-                        iPart == 1);
+                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap);
                 final float start = x + startOffset;
                 // Retract the end position to account for the padding and a point immediately
                 // after.
                 final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
-                        segSegGap, iPart == nParts - 2, hasTrackerIcon);
+                        segSegGap, hasTrackerIcon);
                 final float end = x + segWidth - endOffset;
 
                 drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded));
@@ -927,16 +949,6 @@
                 final float pointWidth = 2 * pointRadius;
                 float start = x - pointRadius;
                 float end = x + pointRadius;
-                // Only shift the points right at the start/end.
-                // For the points close to the start/end, the segment minimum width requirement
-                // would take care of shifting them to be within the bounds.
-                if (iPart == 0) {
-                    start = 0;
-                    end = pointWidth;
-                } else if (iPart == nParts - 1) {
-                    start = totalWidth - pointWidth;
-                    end = totalWidth;
-                }
 
                 drawableParts.add(new DrawablePoint(start, end, point.mColor));
             }
@@ -945,16 +957,13 @@
         return drawableParts;
     }
 
-    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
-            boolean isSecondPart) {
+    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) {
         if (!(prevPart instanceof Point)) return 0F;
-        final float pointOffset = isSecondPart ? pointRadius : 0;
-        return pointOffset + pointRadius + segPointGap;
+        return pointRadius + segPointGap;
     }
 
     private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
-            float segPointGap, float segSegGap, boolean isSecondToLastPart,
-            boolean hasTrackerIcon) {
+            float segPointGap, float segSegGap, boolean hasTrackerIcon) {
         if (nextPart == null) return 0F;
         if (nextPart instanceof Segment nextSeg) {
             if (!seg.mFaded && nextSeg.mFaded) {
@@ -964,8 +973,7 @@
             return segSegGap;
         }
 
-        final float pointOffset = isSecondToLastPart ? pointRadius : 0;
-        return segPointGap + pointRadius + pointOffset;
+        return segPointGap + pointRadius;
     }
 
     /**
@@ -980,7 +988,6 @@
             float segmentMinWidth,
             float pointRadius,
             float progressFraction,
-            float totalWidth,
             boolean isStyledByProgress,
             float progressGap
     ) throws NotEnoughWidthToFitAllPartsException {
@@ -1003,7 +1010,6 @@
                     parts,
                     drawableParts,
                     progressFraction,
-                    totalWidth,
                     isStyledByProgress,
                     progressGap);
         }
@@ -1056,7 +1062,6 @@
                 parts,
                 drawableParts,
                 progressFraction,
-                totalWidth,
                 isStyledByProgress,
                 progressGap);
     }
@@ -1071,11 +1076,12 @@
             List<Part> parts,
             List<DrawablePart> drawableParts,
             float progressFraction,
-            float totalWidth,
             boolean isStyledByProgress,
             float progressGap
     ) {
-        if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth);
+        if (progressFraction == 1) {
+            return new Pair<>(drawableParts, drawableParts.getLast().getEnd());
+        }
 
         int iPartFirstSegmentToStyle = -1;
         int iPartSegmentToSplit = -1;
@@ -1162,14 +1168,15 @@
             float pointRadius,
             boolean hasTrackerIcon,
             float segmentMinWidth,
-            boolean isStyledByProgress
+            boolean isStyledByProgress,
+            int trackerDrawWidth
     ) throws NotEnoughWidthToFitAllPartsException {
         List<Part> parts = processModelAndConvertToViewParts(segments, points, progress,
                 progressMax);
         List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth,
-                segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                segSegGap, segPointGap, pointRadius, hasTrackerIcon, trackerDrawWidth);
         return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius,
-                getProgressFraction(progressMax, progress), totalWidth, isStyledByProgress,
+                getProgressFraction(progressMax, progress), isStyledByProgress,
                 hasTrackerIcon ? 0F : segSegGap);
     }
 
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index d8f1b62..31b9fd1 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -284,6 +284,8 @@
                             displayId.value, modeId);
         ALOGV("receiver %p ~ Returned from Mode Rejected handler.", this);
     }
+
+    mMessageQueue->raiseAndClearException(env, "dispatchModeRejected");
 }
 
 void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
@@ -314,7 +316,7 @@
         ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this);
     }
 
-    mMessageQueue->raiseAndClearException(env, "dispatchModeChanged");
+    mMessageQueue->raiseAndClearException(env, "dispatchFrameRateOverrides");
 }
 
 void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c9f4cdc..5104988 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -9385,6 +9385,10 @@
             android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.server.security.UpdateCertificateRevocationStatusJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
         <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
             android:exported="false">
             <intent-filter>
diff --git a/core/res/res/values-w225dp/dimens.xml b/core/res/res/values-w225dp/dimens.xml
new file mode 100644
index 0000000..0cd3293
--- /dev/null
+++ b/core/res/res/values-w225dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT 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>
+    <!-- The width of the round scrollbar -->
+    <dimen name="round_scrollbar_width">6dp</dimen>
+</resources>
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index 4ff3f88..ef5875e 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -110,4 +110,8 @@
             tap power gesture from triggering the selected target action.
     -->
     <integer name="config_doubleTapPowerGestureMode">0</integer>
+
+    <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
+     config enables OEMs to support its usage across tasks.-->
+    <bool name="config_enableCrossTaskScaleUpAnimation">true</bool>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8db94a4..17acf9a 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3262,6 +3262,14 @@
          as private. {@see android.view.Display#FLAG_PRIVATE} -->
     <integer-array translatable="false" name="config_localPrivateDisplayPorts"></integer-array>
 
+    <!-- Controls if local secondary displays should be able to steal focus and become top display.
+         Value specified in the array represents physical port address of each display and displays
+         in this list due to flag dependencies will be marked with the following flags:
+         {@see android.view.Display#FLAG_STEAL_TOP_FOCUS_DISABLED}
+         {@see android.view.Display#FLAG_OWN_FOCUS} -->
+    <integer-array translatable="false" name="config_localNotStealTopFocusDisplayPorts">
+    </integer-array>
+
     <!-- The default mode for the default display. One of the following values (See Display.java):
              0 - COLOR_MODE_DEFAULT
              7 - COLOR_MODE_SRGB
@@ -7248,6 +7256,9 @@
     <!-- Wear devices: An intent action that is used for remote intent. -->
     <string name="config_wearRemoteIntentAction" translatable="false" />
 
+    <!-- Whether the current device's internal display can host desktop sessions.  -->
+    <bool name="config_canInternalDisplayHostDesktops">false</bool>
+
     <!-- Whether desktop mode is supported on the current device  -->
     <bool name="config_isDesktopModeSupported">false</bool>
 
@@ -7340,4 +7351,23 @@
     <!-- Array containing the notification assistant service adjustments that are not supported by
      default on this device-->
     <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" />
+
+    <!-- Preference name of bugreport-->
+    <string name="prefs_bugreport" translatable="false">bugreports</string>
+
+    <!-- key value of warning state stored in bugreport preference-->
+    <string name="key_warning_state" translatable="false">warning-state</string>
+
+    <!-- Bugreport warning dialog state unknown-->
+    <integer name="bugreport_state_unknown">0</integer>
+
+    <!-- Bugreport warning dialog state shows the warning dialog-->
+    <integer name="bugreport_state_show">1</integer>
+
+    <!-- Bugreport warning dialog state skips the warning dialog-->
+    <integer name="bugreport_state_hide">2</integer>
+
+    <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
+         config enables OEMs to support its usage across tasks.-->
+    <bool name="config_enableCrossTaskScaleUpAnimation">false</bool>
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 484e8ef..595160e 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -782,6 +782,9 @@
          aliasing effects). This is only used on circular displays. -->
     <dimen name="circular_display_mask_thickness">1px</dimen>
 
+    <!-- The width of the round scrollbar -->
+    <dimen name="round_scrollbar_width">5dp</dimen>
+
     <dimen name="lock_pattern_dot_line_width">22dp</dimen>
     <dimen name="lock_pattern_dot_size">14dp</dimen>
     <dimen name="lock_pattern_dot_size_activated">30dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8c2ca97..cc2897a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -430,6 +430,7 @@
   <java-symbol type="bool" name="config_enableProximityService" />
   <java-symbol type="bool" name="config_enableVirtualDeviceManager" />
   <java-symbol type="array" name="config_localPrivateDisplayPorts" />
+  <java-symbol type="array" name="config_localNotStealTopFocusDisplayPorts" />
   <java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" />
   <java-symbol type="bool" name="config_enableAppWidgetService" />
   <java-symbol type="dimen" name="config_pictureInPictureMinAspectRatio" />
@@ -585,6 +586,7 @@
   <java-symbol type="dimen" name="accessibility_magnification_indicator_width" />
   <java-symbol type="dimen" name="circular_display_mask_thickness" />
   <java-symbol type="dimen" name="user_icon_size" />
+  <java-symbol type="dimen" name="round_scrollbar_width" />
 
   <java-symbol type="string" name="add_account_button_label" />
   <java-symbol type="string" name="addToDictionary" />
@@ -5752,6 +5754,9 @@
   <!-- Whether the developer option for desktop mode is supported on the current device  -->
   <java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" />
 
+  <!-- Whether the current device's internal display can host desktop sessions.  -->
+  <java-symbol type="bool" name="config_canInternalDisplayHostDesktops" />
+
   <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. -->
   <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/>
 
@@ -5900,4 +5905,14 @@
   <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" />
   <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" />
   <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" />
+
+  <java-symbol type="string" name="prefs_bugreport" />
+  <java-symbol type="string" name="key_warning_state" />
+  <java-symbol type="integer" name="bugreport_state_unknown" />
+  <java-symbol type="integer" name="bugreport_state_show" />
+  <java-symbol type="integer" name="bugreport_state_hide" />
+
+  <!-- Enable OEMs to support scale up anim across tasks.-->
+  <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" />
+
 </resources>
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index ca6ad6f..f89e441 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -2532,6 +2532,46 @@
 
     @Test
     @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_addProgressPoint_dropsZeroPoints() {
+        // GIVEN
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        // Points should not be a negative integer.
+        progressStyle
+                .addProgressPoint(new Notification.ProgressStyle.Point(0));
+
+        // THEN
+        assertThat(progressStyle.getProgressPoints()).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_setProgressPoint_dropsZeroPoints() {
+        // GIVEN
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        // Points should not be a negative integer.
+        progressStyle
+                .setProgressPoints(List.of(new Notification.ProgressStyle.Point(0)));
+
+        // THEN
+        assertThat(progressStyle.getProgressPoints()).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_createProgressModel_ignoresPointsAtMax() {
+        // GIVEN
+        final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+        progressStyle.addProgressSegment(new Notification.ProgressStyle.Segment(100));
+        // Points should not larger than progress maximum.
+        progressStyle
+                .addProgressPoint(new Notification.ProgressStyle.Point(100));
+
+        // THEN
+        assertThat(progressStyle.createProgressModel(Color.BLUE, Color.RED).getPoints()).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
     public void progressStyle_createProgressModel_ignoresPointsExceedingMax() {
         // GIVEN
         final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
@@ -2573,14 +2613,14 @@
         // THEN
         assertThat(progressStyle.createProgressModel(defaultProgressColor, backgroundColor)
                 .getPoints()).isEqualTo(
-                        List.of(new Notification.ProgressStyle.Point(0)
-                                .setColor(expectedProgressColor),
-                                new Notification.ProgressStyle.Point(20)
+                        List.of(new Notification.ProgressStyle.Point(20)
                                 .setColor(expectedProgressColor),
                                 new Notification.ProgressStyle.Point(50)
                                 .setColor(expectedProgressColor),
                                 new Notification.ProgressStyle.Point(70)
-                                .setColor(expectedProgressColor)
+                                .setColor(expectedProgressColor),
+                                new Notification.ProgressStyle.Point(80)
+                                        .setColor(expectedProgressColor)
                         )
         );
     }
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
index fdfb0c3..fa1948d 100644
--- a/core/tests/coretests/src/android/content/IntentTest.java
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
@@ -238,4 +239,42 @@
         // Not all keys from intent are kept - clip data keys are dropped.
         assertFalse(intent.getExtraIntentKeys().containsAll(originalIntentKeys));
     }
+
+    @Test
+    public void testSetIntentExtrasClassLoaderEffectiveAfterExtraBundleUnparcel() {
+        Intent intent = new Intent();
+        intent.putExtra("bundle", new Bundle());
+
+        final Parcel parcel = Parcel.obtain();
+        intent.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final Intent target = new Intent();
+        target.readFromParcel(parcel);
+        target.collectExtraIntentKeys();
+        ClassLoader cl = new ClassLoader() {
+        };
+        target.setExtrasClassLoader(cl);
+        assertThat(target.getBundleExtra("bundle").getClassLoader()).isEqualTo(cl);
+    }
+
+    @Test
+    public void testBundlePutAllClassLoader() {
+        Intent intent = new Intent();
+        Bundle b1 = new Bundle();
+        b1.putBundle("bundle", new Bundle());
+        intent.putExtra("b1", b1);
+        final Parcel parcel = Parcel.obtain();
+        intent.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final Intent target = new Intent();
+        target.readFromParcel(parcel);
+
+        ClassLoader cl = new ClassLoader() {
+        };
+        target.setExtrasClassLoader(cl);
+        Bundle b2 = new Bundle();
+        b2.putAll(target.getBundleExtra("b1"));
+        assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl);
+    }
+
 }
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8fa5103..dc2f0a6 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -307,8 +307,10 @@
         assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED,
                 mDisplayManagerGlobal
                         .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_ADDED, 0));
-        assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal
-                .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, 0));
+        assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED,
+                mDisplayManagerGlobal
+                        .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED,
+                                0));
         assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED,
                 mDisplayManagerGlobal.mapFiltersToInternalEventFlag(
                         DisplayManager.EVENT_TYPE_DISPLAY_REMOVED, 0));
diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
deleted file mode 100644
index b4f1dee..0000000
--- a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.hardware.input;
-
-import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
-import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests for {@link com.android.hardware.input.Flags}
- *
- * Build/Install/Run:
- *  atest FrameworksCoreTests:InputFlagsTest
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@Presubmit
-public class InputFlagsTest {
-
-    /**
-     * Test that the flags work
-     */
-    @Test
-    public void testFlags() {
-        // No crash when accessing the flag.
-        keyboardLayoutPreviewFlag();
-        keyboardA11yStickyKeysFlag();
-    }
-}
-
diff --git a/core/tests/coretests/src/android/hardware/input/OWNERS b/core/tests/coretests/src/android/hardware/input/OWNERS
deleted file mode 100644
index 3f8a602..0000000
--- a/core/tests/coretests/src/android/hardware/input/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-include /core/java/android/hardware/input/OWNERS
-
diff --git a/core/tests/coretests/src/android/view/WindowManagerTests.java b/core/tests/coretests/src/android/view/WindowManagerTests.java
index 211d768..b32aa46 100644
--- a/core/tests/coretests/src/android/view/WindowManagerTests.java
+++ b/core/tests/coretests/src/android/view/WindowManagerTests.java
@@ -16,9 +16,6 @@
 
 package android.view;
 
-import static com.android.window.flags.Flags.FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG;
-
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -47,19 +44,8 @@
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Test
-    public void testHasWindowExtensionsEnabled_flagDisabled() {
-        mSetFlagsRule.disableFlags(FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG);
-
-        // Before FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG, Extensions are always bundled with AE.
-        assertEquals(isActivityEmbeddingEnableForAll(),
-                WindowManager.hasWindowExtensionsEnabled());
-    }
-
-    @Test
-    public void testHasWindowExtensionsEnabled_flagEnabled() {
-        mSetFlagsRule.enableFlags(FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG);
-
-        // Extensions should be enabled on all devices.
+    public void testHasWindowExtensionsEnabled() {
+        // Extensions should be enabled on all phones/tablets.
         assertTrue(WindowManager.hasWindowExtensionsEnabled());
     }
 
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
index b42bcee..5f89f9c 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -36,11 +36,15 @@
 import android.graphics.Insets;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.autofill.AutofillId;
+import android.view.contentcapture.flags.Flags;
 import android.view.contentprotection.ContentProtectionEventProcessor;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -90,6 +94,8 @@
 
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock private IContentCaptureManager mMockSystemServerInterface;
 
     @Mock private ContentProtectionEventProcessor mMockContentProtectionEventProcessor;
@@ -407,6 +413,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_FLUSH_AFTER_EACH_FRAME)
     @SuppressWarnings("GuardedBy")
     public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled()
             throws RemoteException {
@@ -434,6 +441,34 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_FLUSH_AFTER_EACH_FRAME)
+    @SuppressWarnings("GuardedBy")
+    public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled_Flush()
+            throws RemoteException {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.onSessionStarted(0x2, null);
+        // Override the processor for interaction verification.
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+        notifyContentCaptureEvents(session);
+        mTestableLooper.processAllMessages();
+
+        // Force flush will happen twice.
+        verify(mMockContentCaptureDirectManager, times(1))
+                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any());
+        verify(mMockContentCaptureDirectManager, times(1))
+                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any());
+        // 5 view events + 2 view tree events + 1 flush event
+        verify(mMockContentProtectionEventProcessor, times(8)).processEvent(any());
+        assertThat(session.mEvents).isEmpty();
+    }
+
+    @Test
     public void notifyViewAppearedBelowMaximumBufferSize() throws RemoteException {
         ContentCaptureOptions options =
                 createOptions(
diff --git a/core/tests/coretests/src/android/window/ConfigurationDispatcherTest.kt b/core/tests/coretests/src/android/window/ConfigurationDispatcherTest.kt
new file mode 100644
index 0000000..e8e8a2e
--- /dev/null
+++ b/core/tests/coretests/src/android/window/ConfigurationDispatcherTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.window
+
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.content.ContextWrapper
+import android.content.res.Configuration
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.platform.test.annotations.Presubmit
+import android.view.Display.DEFAULT_DISPLAY
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test to verify [ConfigurationDispatcher]
+ *
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:ConfigurationDispatcherTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(Parameterized::class)
+class ConfigurationDispatcherTest(private val shouldReportPrivateChanges: Boolean) {
+
+    /**
+     * Verifies [ConfigurationDispatcher.shouldReportPrivateChanges].
+     */
+    @Test
+    fun testConfigurationDispatcher() {
+        val receiver = TestConfigurationReceiver(shouldReportPrivateChanges)
+        val config = Configuration().apply {
+            orientation = ORIENTATION_PORTRAIT
+        }
+
+        // Verify public config field change
+        receiver.windowToken.onConfigurationChangedInner(receiver, config, DEFAULT_DISPLAY, true)
+
+        assertThat(receiver.receivedConfig).isEqualTo(config)
+
+        // Clear the config value
+        receiver.receivedConfig.unset()
+
+        // Verify private config field change
+        config.windowConfiguration.windowingMode = WINDOWING_MODE_MULTI_WINDOW
+
+        receiver.windowToken.onConfigurationChangedInner(receiver, config, DEFAULT_DISPLAY, true)
+
+        assertThat(receiver.receivedConfig).isEqualTo(
+            if (shouldReportPrivateChanges) {
+                config
+            } else {
+                Configuration.EMPTY
+            }
+        )
+    }
+
+    /**
+     * Test [android.content.Context] to implement [ConfigurationDispatcher] for testing.
+     *
+     * @param shouldReportPrivateChanges used to override
+     * [ConfigurationDispatcher.shouldReportPrivateChanges] for testing,
+     */
+    private class TestConfigurationReceiver(
+        private val shouldReportPrivateChanges: Boolean
+    ) : ContextWrapper(null), ConfigurationDispatcher {
+        val windowToken = WindowTokenClient()
+        val receivedConfig = Configuration()
+
+        init {
+            windowToken.attachContext(this)
+        }
+
+        override fun dispatchConfigurationChanged(configuration: Configuration) {
+            receivedConfig.setTo(configuration)
+        }
+
+        override fun shouldReportPrivateChanges(): Boolean {
+            return shouldReportPrivateChanges
+        }
+
+        override fun getDisplayId(): Int {
+            return DEFAULT_DISPLAY
+        }
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "shouldReportPrivateChange={0}")
+        @JvmStatic
+        fun data(): Collection<Any> {
+            return listOf(true, false)
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
index 84ff40f..116dc12 100644
--- a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
@@ -266,4 +266,25 @@
 
         verify(mWindowTokenClient).onWindowTokenRemoved();
     }
+
+    @Test
+    public void testOnWindowConfigurationChanged_propagatedToCorrectToken() throws RemoteException {
+        doReturn(mWindowContextInfo).when(mWindowManagerService)
+                .attachWindowContextToDisplayContent(any(), any(), anyInt());
+
+        mController.onWindowConfigurationChanged(mWindowTokenClient, mConfiguration,
+                DEFAULT_DISPLAY + 1);
+
+        // Not propagated before attaching
+        verify(mWindowTokenClient, never()).onConfigurationChanged(mConfiguration,
+                DEFAULT_DISPLAY + 1);
+
+        assertTrue(mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY));
+
+        mController.onWindowConfigurationChanged(mWindowTokenClient, mConfiguration,
+                DEFAULT_DISPLAY + 1);
+
+        // Now that's attached, propagating it.
+        verify(mWindowTokenClient).postOnConfigurationChanged(mConfiguration, DEFAULT_DISPLAY + 1);
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index 9baa31f..282886a 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -121,18 +121,20 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 300, Color.RED)));
+                List.of(new DrawableSegment(10, 310, Color.RED)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -141,14 +143,14 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedRed = 0x80FF0000;
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 300, fadedRed, true)));
+                List.of(new DrawableSegment(10, 310, fadedRed, true)));
 
-        assertThat(p.second).isEqualTo(0);
+        assertThat(p.second).isEqualTo(10);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -168,18 +170,20 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 300, Color.RED)));
+                List.of(new DrawableSegment(10, 310, Color.RED)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -188,9 +192,9 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        assertThat(p.second).isEqualTo(300);
+        assertThat(p.second).isEqualTo(310);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -219,6 +223,42 @@
                 progressMax);
     }
 
+    @Test
+    public void processAndConvertToParts_pointPositionIsZero() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.RED));
+        int progress = 50;
+        int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments,
+                points, progress, progressMax);
+
+        // Point at the start is dropped.
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+    }
+
+    @Test
+    public void processAndConvertToParts_pointPositionAtMax() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(100).setColor(Color.RED));
+        int progress = 50;
+        int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments,
+                points, progress, progressMax);
+
+        // Point at the end is dropped.
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void processAndConvertToParts_pointPositionAboveMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
@@ -249,18 +289,20 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 300, Color.BLUE)));
+                List.of(new DrawableSegment(10, 310, Color.BLUE)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -269,15 +311,15 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedBlue = 0x800000FF;
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 180, Color.BLUE),
-                        new DrawableSegment(180, 300, fadedBlue, true)));
+                List.of(new DrawableSegment(10, 190, Color.BLUE),
+                        new DrawableSegment(190, 310, fadedBlue, true)));
 
-        assertThat(p.second).isEqualTo(180);
+        assertThat(p.second).isEqualTo(190);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -299,19 +341,21 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 146, Color.RED),
-                        new DrawableSegment(150, 300, Color.GREEN)));
+                List.of(new DrawableSegment(10, 156, Color.RED),
+                        new DrawableSegment(160, 310, Color.GREEN)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -319,15 +363,15 @@
         boolean isStyledByProgress = true;
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedGreen = 0x8000FF00;
-        expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED),
-                new DrawableSegment(150, 180, Color.GREEN),
-                new DrawableSegment(180, 300, fadedGreen, true)));
+        expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(10, 156, Color.RED),
+                new DrawableSegment(160, 190, Color.GREEN),
+                new DrawableSegment(190, 310, fadedGreen, true)));
 
-        assertThat(p.second).isEqualTo(180);
+        assertThat(p.second).isEqualTo(190);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -353,10 +397,12 @@
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = false;
+        int trackerDrawWidth = 0;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
                 List.of(new DrawableSegment(0, 146, Color.RED),
@@ -368,7 +414,7 @@
         boolean isStyledByProgress = true;
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedGreen = 0x8000FF00;
@@ -409,26 +455,28 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 35, Color.BLUE),
-                        new DrawablePoint(39, 51, Color.RED),
-                        new DrawableSegment(55, 65, Color.BLUE),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 170, Color.BLUE),
-                        new DrawablePoint(174, 186, Color.BLUE),
-                        new DrawableSegment(190, 215, Color.BLUE),
-                        new DrawablePoint(219, 231, Color.YELLOW),
-                        new DrawableSegment(235, 300, Color.BLUE)));
+                List.of(new DrawableSegment(10, 45, Color.BLUE),
+                        new DrawablePoint(49, 61, Color.RED),
+                        new DrawableSegment(65, 75, Color.BLUE),
+                        new DrawablePoint(79, 91, Color.BLUE),
+                        new DrawableSegment(95, 180, Color.BLUE),
+                        new DrawablePoint(184, 196, Color.BLUE),
+                        new DrawableSegment(200, 225, Color.BLUE),
+                        new DrawablePoint(229, 241, Color.YELLOW),
+                        new DrawableSegment(245, 310, Color.BLUE)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -437,23 +485,23 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedBlue = 0x800000FF;
         int fadedYellow = 0x80FFFF00;
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 34.219177F, Color.BLUE),
-                        new DrawablePoint(38.219177F, 50.219177F, Color.RED),
-                        new DrawableSegment(54.219177F, 70.21918F, Color.BLUE),
-                        new DrawablePoint(74.21918F, 86.21918F, Color.BLUE),
-                        new DrawableSegment(90.21918F, 172.38356F, Color.BLUE),
-                        new DrawablePoint(176.38356F, 188.38356F, Color.BLUE),
-                        new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true),
-                        new DrawablePoint(221.0137F, 233.0137F, fadedYellow),
-                        new DrawableSegment(237.0137F, 300F, fadedBlue, true)));
+                List.of(new DrawableSegment(10, 44.219177F, Color.BLUE),
+                        new DrawablePoint(48.219177F, 60.219177F, Color.RED),
+                        new DrawableSegment(64.219177F, 80.21918F, Color.BLUE),
+                        new DrawablePoint(84.21918F, 96.21918F, Color.BLUE),
+                        new DrawableSegment(100.21918F, 182.38356F, Color.BLUE),
+                        new DrawablePoint(186.38356F, 198.38356F, Color.BLUE),
+                        new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true),
+                        new DrawablePoint(231.0137F, 243.0137F, fadedYellow),
+                        new DrawableSegment(247.0137F, 310F, fadedBlue, true)));
 
-        assertThat(p.second).isEqualTo(182.38356F);
+        assertThat(p.second).isEqualTo(192.38356F);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -488,25 +536,29 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
+
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
-                        new DrawableSegment(55, 65, Color.RED),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 146, Color.RED),
-                        new DrawableSegment(150, 170, Color.GREEN),
-                        new DrawablePoint(174, 186, Color.BLUE),
-                        new DrawableSegment(190, 215, Color.GREEN),
-                        new DrawablePoint(219, 231, Color.YELLOW),
-                        new DrawableSegment(235, 300, Color.GREEN)));
+                List.of(new DrawableSegment(10, 45, Color.RED),
+                        new DrawablePoint(49, 61, Color.RED),
+                        new DrawableSegment(65, 75, Color.RED),
+                        new DrawablePoint(79, 91, Color.BLUE),
+                        new DrawableSegment(95, 156, Color.RED),
+                        new DrawableSegment(160, 180, Color.GREEN),
+                        new DrawablePoint(184, 196, Color.BLUE),
+                        new DrawableSegment(200, 225, Color.GREEN),
+                        new DrawablePoint(229, 241, Color.YELLOW),
+                        new DrawableSegment(245, 310, Color.GREEN)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -515,99 +567,24 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedGreen = 0x8000FF00;
         int fadedYellow = 0x80FFFF00;
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 34.095238F, Color.RED),
-                        new DrawablePoint(38.095238F, 50.095238F, Color.RED),
-                        new DrawableSegment(54.095238F, 70.09524F, Color.RED),
-                        new DrawablePoint(74.09524F, 86.09524F, Color.BLUE),
-                        new DrawableSegment(90.09524F, 148.9524F, Color.RED),
-                        new DrawableSegment(152.95238F, 172.7619F, Color.GREEN),
-                        new DrawablePoint(176.7619F, 188.7619F, Color.BLUE),
-                        new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true),
-                        new DrawablePoint(221.33333F, 233.33333F, fadedYellow),
-                        new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true)));
+                List.of(new DrawableSegment(10, 44.095238F, Color.RED),
+                        new DrawablePoint(48.095238F, 60.095238F, Color.RED),
+                        new DrawableSegment(64.095238F, 80.09524F, Color.RED),
+                        new DrawablePoint(84.09524F, 96.09524F, Color.BLUE),
+                        new DrawableSegment(100.09524F, 158.9524F, Color.RED),
+                        new DrawableSegment(162.95238F, 182.7619F, Color.GREEN),
+                        new DrawablePoint(186.7619F, 198.7619F, Color.BLUE),
+                        new DrawableSegment(202.7619F, 227.33333F, fadedGreen, true),
+                        new DrawablePoint(231.33333F, 243.33333F, fadedYellow),
+                        new DrawableSegment(247.33333F, 309.99997F, fadedGreen, true)));
 
-        assertThat(p.second).isEqualTo(182.7619F);
-        assertThat(p.first).isEqualTo(expectedDrawableParts);
-    }
-
-    @Test
-    public void processAndConvertToParts_multipleSegmentsWithPointsAtStartAndEnd()
-            throws NotEnoughWidthToFitAllPartsException {
-        List<ProgressStyle.Segment> segments = new ArrayList<>();
-        segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
-        segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
-        List<ProgressStyle.Point> points = new ArrayList<>();
-        points.add(new ProgressStyle.Point(0).setColor(Color.RED));
-        points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
-        points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
-        points.add(new ProgressStyle.Point(100).setColor(Color.YELLOW));
-        int progress = 60;
-        int progressMax = 100;
-
-        List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments,
-                points, progress, progressMax);
-
-        List<Part> expectedParts = new ArrayList<>(
-                List.of(new Point(Color.RED),
-                        new Segment(0.25f, Color.RED),
-                        new Point(Color.BLUE),
-                        new Segment(0.25f, Color.RED),
-                        new Segment(0.10f, Color.GREEN),
-                        new Point(Color.BLUE),
-                        new Segment(0.4f, Color.GREEN),
-                        new Point(Color.YELLOW)));
-
-        assertThat(parts).isEqualTo(expectedParts);
-
-        float drawableWidth = 300;
-        float segSegGap = 4;
-        float segPointGap = 4;
-        float pointRadius = 6;
-        boolean hasTrackerIcon = true;
-
-        List<DrawablePart> drawableParts =
-                NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
-
-        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawablePoint(0, 12, Color.RED),
-                        new DrawableSegment(16, 65, Color.RED),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 146, Color.RED),
-                        new DrawableSegment(150, 170, Color.GREEN),
-                        new DrawablePoint(174, 186, Color.BLUE),
-                        new DrawableSegment(190, 284, Color.GREEN),
-                        new DrawablePoint(288, 300, Color.YELLOW)));
-
-        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
-
-        float segmentMinWidth = 16;
-        boolean isStyledByProgress = true;
-
-        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
-                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
-
-        // Colors with 50% opacity
-        int fadedGreen = 0x8000FF00;
-        int fadedYellow = 0x80FFFF00;
-        expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawablePoint(0, 12, Color.RED),
-                        new DrawableSegment(16, 65, Color.RED),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 146, Color.RED),
-                        new DrawableSegment(150, 170, Color.GREEN),
-                        new DrawablePoint(174, 186, Color.BLUE),
-                        new DrawableSegment(190, 284, fadedGreen, true),
-                        new DrawablePoint(288, 300, fadedYellow)));
-
-        assertThat(p.second).isEqualTo(180);
+        assertThat(p.second).isEqualTo(192.7619F);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -644,27 +621,29 @@
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, -7, Color.RED),
-                        new DrawablePoint(-3, 9, Color.RED),
-                        new DrawableSegment(13, 65, Color.RED),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 146, Color.RED),
-                        new DrawableSegment(150, 170, Color.GREEN),
-                        new DrawablePoint(174, 186, Color.BLUE),
-                        new DrawableSegment(190, 287, Color.GREEN),
-                        new DrawablePoint(291, 303, Color.YELLOW),
-                        new DrawableSegment(307, 300, Color.GREEN)));
+                List.of(new DrawableSegment(10, 3, Color.RED),
+                        new DrawablePoint(7, 19, Color.RED),
+                        new DrawableSegment(23, 75, Color.RED),
+                        new DrawablePoint(79, 91, Color.BLUE),
+                        new DrawableSegment(95, 156, Color.RED),
+                        new DrawableSegment(160, 180, Color.GREEN),
+                        new DrawablePoint(184, 196, Color.BLUE),
+                        new DrawableSegment(200, 297, Color.GREEN),
+                        new DrawablePoint(301, 313, Color.YELLOW),
+                        new DrawableSegment(317, 310, Color.GREEN)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -673,24 +652,24 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 50% opacity
         int fadedGreen = 0x8000FF00;
         int fadedYellow = 0x80FFFF00;
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 16, Color.RED),
-                        new DrawablePoint(20, 32, Color.RED),
-                        new DrawableSegment(36, 78.02409F, Color.RED),
-                        new DrawablePoint(82.02409F, 94.02409F, Color.BLUE),
-                        new DrawableSegment(98.02409F, 146.55421F, Color.RED),
-                        new DrawableSegment(150.55421F, 169.44579F, Color.GREEN),
-                        new DrawablePoint(173.44579F, 185.44579F, Color.BLUE),
-                        new DrawableSegment(189.44579F, 264, fadedGreen, true),
-                        new DrawablePoint(268, 280, fadedYellow),
-                        new DrawableSegment(284, 300, fadedGreen, true)));
+                List.of(new DrawableSegment(10, 26, Color.RED),
+                        new DrawablePoint(30, 42, Color.RED),
+                        new DrawableSegment(46, 88.02409F, Color.RED),
+                        new DrawablePoint(92.02409F, 104.02409F, Color.BLUE),
+                        new DrawableSegment(108.02409F, 156.55421F, Color.RED),
+                        new DrawableSegment(160.55421F, 179.44579F, Color.GREEN),
+                        new DrawablePoint(183.44579F, 195.44579F, Color.BLUE),
+                        new DrawableSegment(199.44579F, 274, fadedGreen, true),
+                        new DrawablePoint(278, 290, fadedYellow),
+                        new DrawableSegment(294, 310, fadedGreen, true)));
 
-        assertThat(p.second).isEqualTo(179.44579F);
+        assertThat(p.second).isEqualTo(189.44579F);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -711,31 +690,38 @@
                 points, progress, progressMax);
 
         List<Part> expectedParts = new ArrayList<>(
-                List.of(new Segment(0.15f, Color.RED), new Point(Color.RED),
-                        new Segment(0.10f, Color.RED), new Point(Color.BLUE),
-                        new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN),
-                        new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN)));
+                List.of(new Segment(0.15f, Color.RED),
+                        new Point(Color.RED),
+                        new Segment(0.10f, Color.RED),
+                        new Point(Color.BLUE),
+                        new Segment(0.25f, Color.RED),
+                        new Segment(0.25f, Color.GREEN),
+                        new Point(Color.YELLOW),
+                        new Segment(0.25f, Color.GREEN)));
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
-                        new DrawableSegment(55, 65, Color.RED),
-                        new DrawablePoint(69, 81, Color.BLUE),
-                        new DrawableSegment(85, 146, Color.RED),
-                        new DrawableSegment(150, 215, Color.GREEN),
-                        new DrawablePoint(219, 231, Color.YELLOW),
-                        new DrawableSegment(235, 300, Color.GREEN)));
+                List.of(new DrawableSegment(10, 45, Color.RED),
+                        new DrawablePoint(49, 61, Color.RED),
+                        new DrawableSegment(65, 75, Color.RED),
+                        new DrawablePoint(79, 91, Color.BLUE),
+                        new DrawableSegment(95, 156, Color.RED),
+                        new DrawableSegment(160, 225, Color.GREEN),
+                        new DrawablePoint(229, 241, Color.YELLOW),
+                        new DrawableSegment(245, 310, Color.GREEN)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -744,34 +730,34 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 34.296295F, Color.RED),
-                        new DrawablePoint(38.296295F, 50.296295F, Color.RED),
-                        new DrawableSegment(54.296295F, 70.296295F, Color.RED),
-                        new DrawablePoint(74.296295F, 86.296295F, Color.BLUE),
-                        new DrawableSegment(90.296295F, 149.62962F, Color.RED),
-                        new DrawableSegment(153.62962F, 216.8148F, Color.GREEN),
-                        new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW),
-                        new DrawableSegment(236.81482F, 300, Color.GREEN)));
+                List.of(new DrawableSegment(10, 44.296295F, Color.RED),
+                        new DrawablePoint(48.296295F, 60.296295F, Color.RED),
+                        new DrawableSegment(64.296295F, 80.296295F, Color.RED),
+                        new DrawablePoint(84.296295F, 96.296295F, Color.BLUE),
+                        new DrawableSegment(100.296295F, 159.62962F, Color.RED),
+                        new DrawableSegment(163.62962F, 226.8148F, Color.GREEN),
+                        new DrawablePoint(230.81482F, 242.81482F, Color.YELLOW),
+                        new DrawableSegment(246.81482F, 310, Color.GREEN)));
 
-        assertThat(p.second).isEqualTo(182.9037F);
+        assertThat(p.second).isEqualTo(192.9037F);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
-    // The only difference from the `zeroWidthDrawableSegment` test below is the longer
+    // The only difference from the `segmentWidthAtMin` test below is the longer
     // segmentMinWidth (= 16dp).
     @Test
-    public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment()
+    public void maybeStretchAndRescaleSegments_segmentWidthBelowMin()
             throws NotEnoughWidthToFitAllPartsException {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
-        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
         List<ProgressStyle.Point> points = new ArrayList<>();
-        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        points.add(new ProgressStyle.Point(200).setColor(Color.BLUE));
         int progress = 1000;
         int progressMax = 1000;
 
@@ -779,28 +765,32 @@
                 points, progress, progressMax);
 
         List<Part> expectedParts = new ArrayList<>(
-                List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
-                        new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                List.of(new Segment(0.2f, Color.BLUE),
+                        new Point(Color.BLUE),
+                        new Segment(0.1f, Color.BLUE),
+                        new Segment(0.3f, Color.BLUE),
                         new Segment(0.4f, Color.BLUE)));
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 200;
+        float drawableWidth = 220;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawablePoint(0, 12, Color.BLUE),
-                        new DrawableSegment(16, 16, Color.BLUE),
-                        new DrawableSegment(20, 56, Color.BLUE),
-                        new DrawableSegment(60, 116, Color.BLUE),
-                        new DrawableSegment(120, 200, Color.BLUE)));
+                List.of(new DrawableSegment(10, 40, Color.BLUE),
+                        new DrawablePoint(44, 56, Color.BLUE),
+                        new DrawableSegment(60, 66, Color.BLUE),
+                        new DrawableSegment(70, 126, Color.BLUE),
+                        new DrawableSegment(130, 210, Color.BLUE)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -809,30 +799,31 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
-                new DrawableSegment(16, 32, Color.BLUE),
-                new DrawableSegment(36, 69.41936F, Color.BLUE),
-                new DrawableSegment(73.41936F, 124.25807F, Color.BLUE),
-                new DrawableSegment(128.25807F, 200, Color.BLUE)));
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(10, 38.81356F, Color.BLUE),
+                        new DrawablePoint(42.81356F, 54.81356F, Color.BLUE),
+                        new DrawableSegment(58.81356F, 74.81356F, Color.BLUE),
+                        new DrawableSegment(78.81356F, 131.42374F, Color.BLUE),
+                        new DrawableSegment(135.42374F, 210, Color.BLUE)));
 
-        assertThat(p.second).isEqualTo(200);
+        assertThat(p.second).isEqualTo(210);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
-    // The only difference from the `negativeWidthDrawableSegment` test above is the shorter
+    // The only difference from the `segmentWidthBelowMin` test above is the shorter
     // segmentMinWidth (= 10dp).
     @Test
-    public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment()
+    public void maybeStretchAndRescaleSegments_segmentWidthAtMin()
             throws NotEnoughWidthToFitAllPartsException {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
-        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
         List<ProgressStyle.Point> points = new ArrayList<>();
-        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        points.add(new ProgressStyle.Point(200).setColor(Color.BLUE));
         int progress = 1000;
         int progressMax = 1000;
 
@@ -840,28 +831,32 @@
                 points, progress, progressMax);
 
         List<Part> expectedParts = new ArrayList<>(
-                List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
-                        new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                List.of(new Segment(0.2f, Color.BLUE),
+                        new Point(Color.BLUE),
+                        new Segment(0.1f, Color.BLUE),
+                        new Segment(0.3f, Color.BLUE),
                         new Segment(0.4f, Color.BLUE)));
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 200;
+        float drawableWidth = 220;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawablePoint(0, 12, Color.BLUE),
-                        new DrawableSegment(16, 16, Color.BLUE),
-                        new DrawableSegment(20, 56, Color.BLUE),
-                        new DrawableSegment(60, 116, Color.BLUE),
-                        new DrawableSegment(120, 200, Color.BLUE)));
+                List.of(new DrawableSegment(10, 40, Color.BLUE),
+                        new DrawablePoint(44, 56, Color.BLUE),
+                        new DrawableSegment(60, 66, Color.BLUE),
+                        new DrawableSegment(70, 126, Color.BLUE),
+                        new DrawableSegment(130, 210, Color.BLUE)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -870,15 +865,16 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
-                new DrawableSegment(16, 26, Color.BLUE),
-                new DrawableSegment(30, 64.169014F, Color.BLUE),
-                new DrawableSegment(68.169014F, 120.92958F, Color.BLUE),
-                new DrawableSegment(124.92958F, 200, Color.BLUE)));
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(10, 39.411766F, Color.BLUE),
+                        new DrawablePoint(43.411766F, 55.411766F, Color.BLUE),
+                        new DrawableSegment(59.411766F, 69.411766F, Color.BLUE),
+                        new DrawableSegment(73.411766F, 128.05884F, Color.BLUE),
+                        new DrawableSegment(132.05882F, 210, Color.BLUE)));
 
-        assertThat(p.second).isEqualTo(200);
+        assertThat(p.second).isEqualTo(210);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -886,12 +882,12 @@
     public void maybeStretchAndRescaleSegments_noStretchingNecessary()
             throws NotEnoughWidthToFitAllPartsException {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
-        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
         segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
         List<ProgressStyle.Point> points = new ArrayList<>();
-        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        points.add(new ProgressStyle.Point(100).setColor(Color.BLUE));
         int progress = 1000;
         int progressMax = 1000;
 
@@ -899,28 +895,32 @@
                 points, progress, progressMax);
 
         List<Part> expectedParts = new ArrayList<>(
-                List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE),
-                        new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                List.of(new Segment(0.1f, Color.BLUE),
+                        new Point(Color.BLUE),
+                        new Segment(0.2f, Color.BLUE),
+                        new Segment(0.3f, Color.BLUE),
                         new Segment(0.4f, Color.BLUE)));
 
         assertThat(parts).isEqualTo(expectedParts);
 
-        float drawableWidth = 200;
+        float drawableWidth = 220;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawablePoint(0, 12, Color.BLUE),
-                        new DrawableSegment(16, 36, Color.BLUE),
-                        new DrawableSegment(40, 56, Color.BLUE),
-                        new DrawableSegment(60, 116, Color.BLUE),
-                        new DrawableSegment(120, 200, Color.BLUE)));
+                List.of(new DrawableSegment(10, 20, Color.BLUE),
+                        new DrawablePoint(24, 36, Color.BLUE),
+                        new DrawableSegment(40, 66, Color.BLUE),
+                        new DrawableSegment(70, 126, Color.BLUE),
+                        new DrawableSegment(130, 210, Color.BLUE)));
 
         assertThat(drawableParts).isEqualTo(expectedDrawableParts);
 
@@ -929,9 +929,9 @@
 
         Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        assertThat(p.second).isEqualTo(200);
+        assertThat(p.second).isEqualTo(210);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -951,10 +951,10 @@
         segments.add(new ProgressStyle.Segment(10).setColor(Color.GREEN));
         segments.add(new ProgressStyle.Segment(10).setColor(Color.RED));
         List<ProgressStyle.Point> points = new ArrayList<>();
-        points.add(new ProgressStyle.Point(0).setColor(orange));
         points.add(new ProgressStyle.Point(1).setColor(Color.BLUE));
+        points.add(new ProgressStyle.Point(10).setColor(orange));
         points.add(new ProgressStyle.Point(55).setColor(Color.BLUE));
-        points.add(new ProgressStyle.Point(100).setColor(orange));
+        points.add(new ProgressStyle.Point(90).setColor(orange));
         int progress = 50;
         int progressMax = 100;
 
@@ -962,10 +962,10 @@
                 points, progress, progressMax);
 
         List<Part> expectedParts = new ArrayList<>(
-                List.of(new Point(orange),
-                        new Segment(0.01f, orange),
+                List.of(new Segment(0.01f, orange),
                         new Point(Color.BLUE),
                         new Segment(0.09f, orange),
+                        new Point(orange),
                         new Segment(0.1f, Color.YELLOW),
                         new Segment(0.1f, Color.BLUE),
                         new Segment(0.1f, Color.GREEN),
@@ -976,21 +976,23 @@
                         new Segment(0.1f, Color.YELLOW),
                         new Segment(0.1f, Color.BLUE),
                         new Segment(0.1f, Color.GREEN),
-                        new Segment(0.1f, Color.RED),
-                        new Point(orange)));
+                        new Point(orange),
+                        new Segment(0.1f, Color.RED)));
 
         assertThat(parts).isEqualTo(expectedParts);
 
         // For the list of ProgressStyle.Part used in this test, 300 is the minimum width.
-        float drawableWidth = 299;
+        float drawableWidth = 319;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         List<DrawablePart> drawableParts =
                 NotificationProgressBar.processPartsAndConvertToDrawableParts(
-                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+                        parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon,
+                        trackerDrawWidth);
 
         // Skips the validation of the intermediate list of DrawableParts.
 
@@ -999,7 +1001,7 @@
 
         NotificationProgressBar.maybeStretchAndRescaleSegments(
                 parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
-                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+                isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
     }
 
     @Test
@@ -1015,11 +1017,12 @@
         int progress = 60;
         int progressMax = 100;
 
-        float drawableWidth = 300;
+        float drawableWidth = 320;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
@@ -1036,24 +1039,24 @@
                         pointRadius,
                         hasTrackerIcon,
                         segmentMinWidth,
-                        isStyledByProgress
-                );
+                        isStyledByProgress,
+                        trackerDrawWidth);
 
         // Colors with 50% opacity
         int fadedBlue = 0x800000FF;
         int fadedYellow = 0x80FFFF00;
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 34.219177F, Color.BLUE),
-                        new DrawablePoint(38.219177F, 50.219177F, Color.RED),
-                        new DrawableSegment(54.219177F, 70.21918F, Color.BLUE),
-                        new DrawablePoint(74.21918F, 86.21918F, Color.BLUE),
-                        new DrawableSegment(90.21918F, 172.38356F, Color.BLUE),
-                        new DrawablePoint(176.38356F, 188.38356F, Color.BLUE),
-                        new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true),
-                        new DrawablePoint(221.0137F, 233.0137F, fadedYellow),
-                        new DrawableSegment(237.0137F, 300F, fadedBlue, true)));
+                List.of(new DrawableSegment(10, 44.219177F, Color.BLUE),
+                        new DrawablePoint(48.219177F, 60.219177F, Color.RED),
+                        new DrawableSegment(64.219177F, 80.21918F, Color.BLUE),
+                        new DrawablePoint(84.21918F, 96.21918F, Color.BLUE),
+                        new DrawableSegment(100.21918F, 182.38356F, Color.BLUE),
+                        new DrawablePoint(186.38356F, 198.38356F, Color.BLUE),
+                        new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true),
+                        new DrawablePoint(231.0137F, 243.0137F, fadedYellow),
+                        new DrawableSegment(247.0137F, 310F, fadedBlue, true)));
 
-        assertThat(p.second).isEqualTo(182.38356F);
+        assertThat(p.second).isEqualTo(192.38356F);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
@@ -1065,11 +1068,12 @@
         int progress = 60;
         int progressMax = 100;
 
-        float drawableWidth = 100;
+        float drawableWidth = 120;
         float segSegGap = 4;
         float segPointGap = 4;
         float pointRadius = 6;
         boolean hasTrackerIcon = true;
+        int trackerDrawWidth = 20;
 
         float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
@@ -1086,16 +1090,16 @@
                         pointRadius,
                         hasTrackerIcon,
                         segmentMinWidth,
-                        isStyledByProgress
-                );
+                        isStyledByProgress,
+                        trackerDrawWidth);
 
-        // Colors with 50%f opacity
+        // Colors with 50% opacity
         int fadedBlue = 0x800000FF;
         List<DrawablePart> expectedDrawableParts = new ArrayList<>(
-                List.of(new DrawableSegment(0, 60.000004F, Color.BLUE),
-                        new DrawableSegment(60.000004F, 100, fadedBlue, true)));
+                List.of(new DrawableSegment(10, 70F, Color.BLUE),
+                        new DrawableSegment(70F, 110, fadedBlue, true)));
 
-        assertThat(p.second).isWithin(1e-5f).of(60);
+        assertThat(p.second).isEqualTo(70);
         assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java
index e1f5b1c..140d268 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java
@@ -90,7 +90,7 @@
                 new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW),
                 new Notification.ProgressStyle.Segment(50).setColor(Color.LTGRAY));
         final List<Notification.ProgressStyle.Point> points = List.of(
-                new Notification.ProgressStyle.Point(0).setColor(Color.RED),
+                new Notification.ProgressStyle.Point(1).setColor(Color.RED),
                 new Notification.ProgressStyle.Point(20).setColor(Color.BLUE));
         final NotificationProgressModel savedModel = new NotificationProgressModel(segments,
                 points,
@@ -121,7 +121,7 @@
                 new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW),
                 new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW));
         final List<Notification.ProgressStyle.Point> points = List.of(
-                new Notification.ProgressStyle.Point(0).setColor(Color.RED),
+                new Notification.ProgressStyle.Point(1).setColor(Color.RED),
                 new Notification.ProgressStyle.Point(20).setColor(Color.BLUE));
         final NotificationProgressModel savedModel = new NotificationProgressModel(segments,
                 points,
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index dfded73..0c4ea79 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -102,6 +102,10 @@
 
     private static volatile int sDefaultDensity = -1;
 
+    /**
+     * This id is not authoritative and can be duplicated if an ashmem bitmap is decoded from a
+     * parcel.
+     */
     private long mId;
 
     /**
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 13d0169..a08f88a 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -177,3 +177,10 @@
     description: "Factor task-view state tracking out of taskviewtransitions"
     bug: "384976265"
 }
+
+flag {
+    name: "enable_bubble_bar_on_phones"
+    namespace: "multitasking"
+    description: "Try out bubble bar on phones"
+    bug: "394869612"
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
index bce6c59..a32ec22 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
@@ -61,7 +61,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import java.util.Optional
@@ -133,7 +132,7 @@
                 mainExecutor,
                 bgExecutor,
             )
-        bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java))
+        bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>())
 
         shellInit.init()
 
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 88bfeb2..e865111 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -50,10 +50,10 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito
 import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
@@ -635,7 +635,7 @@
 
     @Test
     fun removeFromWindow_stopMonitoringSwipeUpGesture() {
-        bubbleStackView = Mockito.spy(bubbleStackView)
+        bubbleStackView = spy(bubbleStackView)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             // No way to add to window in the test environment right now so just pretend
             bubbleStackView.onDetachedFromWindow()
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt
index af238d0..3499ee3 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt
@@ -29,7 +29,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
 /** Test for [UiEventSubject] */
@@ -130,10 +131,10 @@
     }
 
     private fun createBubble(appUid: Int, packageName: String, instanceId: InstanceId): Bubble {
-        return mock(Bubble::class.java).apply {
-            whenever(getAppUid()).thenReturn(appUid)
-            whenever(getPackageName()).thenReturn(packageName)
-            whenever(getInstanceId()).thenReturn(instanceId)
+        return mock<Bubble>() {
+            on { getAppUid() } doReturn appUid
+            on { getPackageName() } doReturn packageName
+            on { getInstanceId() } doReturn instanceId
         }
     }
 
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index c022a29..7b583137 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -73,7 +73,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -127,7 +126,7 @@
                 mainExecutor,
                 bgExecutor,
             )
-        bubbleController.asBubbles().setSysuiProxy(mock(SysuiProxy::class.java))
+        bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>())
         // Flush so that proxy gets set
         mainExecutor.flushAll()
 
diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
index ce24275..05c1e09 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
@@ -13,20 +13,10 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<vector
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="128dp"
-    android:height="4dp"
-    android:viewportWidth="128"
-    android:viewportHeight="4"
-    >
-    <group>
-        <clip-path
-            android:pathData="M2 0H126C127.105 0 128 0.895431 128 2C128 3.10457 127.105 4 126 4H2C0.895431 4 0 3.10457 0 2C0 0.895431 0.895431 0 2 0Z"
-            />
-        <path
-            android:pathData="M0 0V4H128V0"
-            android:fillColor="@android:color/black"
-            />
-    </group>
-</vector>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/black"/>
+    <corners android:radius="2dp"/>
+    <size android:height="4dp"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
index 1d1cdfa..9451fd4 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
@@ -20,7 +20,7 @@
     android:id="@+id/desktop_mode_caption"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:gravity="center_horizontal">
+    android:gravity="center">
 
     <com.android.wm.shell.windowdecor.HandleImageButton
         android:id="@+id/caption_handle"
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index a2231dd..1b7daa8 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -290,7 +290,7 @@
     <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
     <string name="fullscreen_text">Fullscreen</string>
     <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
-    <string name="desktop_text">Desktop Mode</string>
+    <string name="desktop_text">Desktop View</string>
     <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] -->
     <string name="split_screen_text">Split Screen</string>
     <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] -->
@@ -316,7 +316,7 @@
     <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
     <string name="collapse_menu_text">Close Menu</string>
     <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] -->
-    <string name="desktop_mode_app_header_chip_text">Open Menu</string>
+    <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string>
     <!-- Maximize menu maximize button string. -->
     <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
     <!-- Maximize menu snap buttons string. -->
@@ -342,10 +342,10 @@
     <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] -->
     <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string>
 
-    <!-- Accessibility action replacement for caption handle menu split screen button [CHAR LIMIT=NONE] -->
-    <string name="app_handle_menu_talkback_split_screen_mode_button_text">Enter split screen mode</string>
-    <!-- Accessibility action replacement for caption handle menu enter desktop mode button [CHAR LIMIT=NONE] -->
-    <string name="app_handle_menu_talkback_desktop_mode_button_text">Enter desktop windowing mode</string>
+    <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] -->
+    <string name="app_handle_chip_accessibility_announce">Open Menu</string>
+    <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] -->
+    <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string>
     <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] -->
     <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string>
     <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index 0b1f76f..d280083 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -17,4 +17,23 @@
 <resources>
     <dimen name="floating_dismiss_icon_size">32dp</dimen>
     <dimen name="floating_dismiss_background_size">96dp</dimen>
+
+    <!-- Bubble drag zone dimensions -->
+    <dimen name="drag_zone_dismiss_fold">140dp</dimen>
+    <dimen name="drag_zone_dismiss_tablet">200dp</dimen>
+    <dimen name="drag_zone_bubble_fold">140dp</dimen>
+    <dimen name="drag_zone_bubble_tablet">200dp</dimen>
+    <dimen name="drag_zone_full_screen_width">512dp</dimen>
+    <dimen name="drag_zone_full_screen_height">44dp</dimen>
+    <dimen name="drag_zone_desktop_window_width">880dp</dimen>
+    <dimen name="drag_zone_desktop_window_height">300dp</dimen>
+    <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen>
+    <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen>
+    <dimen name="drag_zone_split_from_bubble_height">100dp</dimen>
+    <dimen name="drag_zone_split_from_bubble_width">60dp</dimen>
+    <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen>
+    <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen>
+    <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen>
+    <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen>
+    <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index aa523f5..909e9d2 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -16,11 +16,15 @@
 
 package com.android.wm.shell.shared.bubbles
 
+import android.content.Context
 import android.graphics.Rect
+import androidx.annotation.DimenRes
+import com.android.wm.shell.shared.R
 import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
 
 /** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
 class DragZoneFactory(
+    private val context: Context,
     private val deviceConfig: DeviceConfig,
     private val splitScreenModeChecker: SplitScreenModeChecker,
     private val desktopWindowModeChecker: DesktopWindowModeChecker,
@@ -29,23 +33,65 @@
     private val windowBounds: Rect
         get() = deviceConfig.windowBounds
 
-    // TODO b/393172431: move these to xml
-    private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200
-    private val bubbleDragZoneTabletSize = 200
-    private val bubbleDragZoneFoldableSize = 140
-    private val fullScreenDragZoneWidth = 512
-    private val fullScreenDragZoneHeight = 44
-    private val desktopWindowDragZoneWidth = 880
-    private val desktopWindowDragZoneHeight = 300
-    private val desktopWindowFromExpandedViewDragZoneWidth = 200
-    private val desktopWindowFromExpandedViewDragZoneHeight = 350
-    private val splitFromBubbleDragZoneHeight = 100
-    private val splitFromBubbleDragZoneWidth = 60
-    private val hSplitFromExpandedViewDragZoneWidth = 60
-    private val vSplitFromExpandedViewDragZoneWidth = 200
-    private val vSplitFromExpandedViewDragZoneHeightTablet = 285
-    private val vSplitFromExpandedViewDragZoneHeightFoldTall = 150
-    private val vSplitFromExpandedViewDragZoneHeightFoldShort = 100
+    private var dismissDragZoneSize = 0
+    private var bubbleDragZoneTabletSize = 0
+    private var bubbleDragZoneFoldableSize = 0
+    private var fullScreenDragZoneWidth = 0
+    private var fullScreenDragZoneHeight = 0
+    private var desktopWindowDragZoneWidth = 0
+    private var desktopWindowDragZoneHeight = 0
+    private var desktopWindowFromExpandedViewDragZoneWidth = 0
+    private var desktopWindowFromExpandedViewDragZoneHeight = 0
+    private var splitFromBubbleDragZoneHeight = 0
+    private var splitFromBubbleDragZoneWidth = 0
+    private var hSplitFromExpandedViewDragZoneWidth = 0
+    private var vSplitFromExpandedViewDragZoneWidth = 0
+    private var vSplitFromExpandedViewDragZoneHeightTablet = 0
+    private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
+    private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
+
+    init {
+        onConfigurationUpdated()
+    }
+
+    /** Updates all dimensions after a configuration change. */
+    fun onConfigurationUpdated() {
+        dismissDragZoneSize =
+            if (deviceConfig.isSmallTablet) {
+                context.resolveDimension(R.dimen.drag_zone_dismiss_fold)
+            } else {
+                context.resolveDimension(R.dimen.drag_zone_dismiss_tablet)
+            }
+        bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet)
+        bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold)
+        fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width)
+        fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height)
+        desktopWindowDragZoneWidth =
+            context.resolveDimension(R.dimen.drag_zone_desktop_window_width)
+        desktopWindowDragZoneHeight =
+            context.resolveDimension(R.dimen.drag_zone_desktop_window_height)
+        desktopWindowFromExpandedViewDragZoneWidth =
+            context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width)
+        desktopWindowFromExpandedViewDragZoneHeight =
+            context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height)
+        splitFromBubbleDragZoneHeight =
+            context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height)
+        splitFromBubbleDragZoneWidth =
+            context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width)
+        hSplitFromExpandedViewDragZoneWidth =
+            context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width)
+        vSplitFromExpandedViewDragZoneWidth =
+            context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width)
+        vSplitFromExpandedViewDragZoneHeightTablet =
+            context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet)
+        vSplitFromExpandedViewDragZoneHeightFoldTall =
+            context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall)
+        vSplitFromExpandedViewDragZoneHeightFoldShort =
+            context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short)
+    }
+
+    private fun Context.resolveDimension(@DimenRes dimension: Int) =
+        resources.getDimensionPixelSize(dimension)
 
     /**
      * Creates the list of drag zones for the dragged object.
@@ -58,11 +104,11 @@
         when (draggedObject) {
             is DraggedObject.BubbleBar -> {
                 dragZones.add(createDismissDragZone())
-                dragZones.addAll(createBubbleDragZones())
+                dragZones.addAll(createBubbleHalfScreenDragZones())
             }
             is DraggedObject.Bubble -> {
                 dragZones.add(createDismissDragZone())
-                dragZones.addAll(createBubbleDragZones())
+                dragZones.addAll(createBubbleCornerDragZones())
                 dragZones.add(createFullScreenDragZone())
                 if (shouldShowDesktopWindowDragZones()) {
                     dragZones.add(createDesktopWindowDragZoneForBubble())
@@ -80,7 +126,7 @@
                 } else {
                     dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
                 }
-                createBubbleDragZonesForExpandedView()
+                dragZones.addAll(createBubbleHalfScreenDragZones())
             }
         }
         return dragZones
@@ -98,7 +144,7 @@
         )
     }
 
-    private fun createBubbleDragZones(): List<DragZone> {
+    private fun createBubbleCornerDragZones(): List<DragZone> {
         val dragZoneSize =
             if (deviceConfig.isSmallTablet) {
                 bubbleDragZoneFoldableSize
@@ -124,7 +170,7 @@
         )
     }
 
-    private fun createBubbleDragZonesForExpandedView(): List<DragZone> {
+    private fun createBubbleHalfScreenDragZones(): List<DragZone> {
         return listOf(
             DragZone.Bubble.Left(
                 bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
new file mode 100644
index 0000000..29ce8d9
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+/**
+ * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
+ * across different drag zones.
+ */
+class DropTargetManager(
+    private val isLayoutRtl: Boolean,
+    private val dragZoneChangedListener: DragZoneChangedListener
+) {
+
+    private var state: DragState? = null
+
+    /** Must be called when a drag gesture is starting. */
+    fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
+        val state = DragState(dragZones, draggedObject)
+        dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
+        this.state = state
+    }
+
+    /** Called when the user drags to a new location. */
+    fun onDragUpdated(x: Int, y: Int) {
+        val state = state ?: return
+        val oldDragZone = state.currentDragZone
+        val newDragZone = state.getMatchingDragZone(x = x, y = y)
+        state.currentDragZone = newDragZone
+        if (oldDragZone != newDragZone) {
+            dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone)
+        }
+    }
+
+    /** Called when the drag ended. */
+    fun onDragEnded() {
+        state = null
+    }
+
+    /** Stores the current drag state. */
+    private inner class DragState(
+        private val dragZones: List<DragZone>,
+        draggedObject: DraggedObject
+    ) {
+        val initialDragZone =
+            if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
+                dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
+            } else {
+                dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
+            }
+        var currentDragZone: DragZone = initialDragZone
+
+        fun getMatchingDragZone(x: Int, y: Int): DragZone {
+            return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
+        }
+    }
+
+    /** An interface to be notified when drag zones change. */
+    interface DragZoneChangedListener {
+        /** An initial drag zone was set. Called when a drag starts. */
+        fun onInitialDragZoneSet(dragZone: DragZone)
+        /** Called when the object was dragged to a different drag zone. */
+        fun onDragZoneChanged(from: DragZone, to: DragZone)
+    }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
index f234ff5c2c..126ab3d 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
@@ -21,6 +21,7 @@
 import android.content.pm.ActivityInfo
 import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED
 import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION
+import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
 import android.window.DesktopModeFlags
 import com.android.internal.R
 import com.android.window.flags.Flags
@@ -49,7 +50,7 @@
         numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) =
         DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue
                 && ((isSystemUiTask(packageName)
-                || isPartOfDefaultHomePackage(packageName)
+                || isPartOfDefaultHomePackageOrNoHomeAvailable(packageName)
                 || isTransparentTask(isActivityStackTransparent, numActivities))
                 && !isTopActivityNoDisplay)
 
@@ -59,13 +60,16 @@
      * The treatment is enabled when all the of the following is true:
      * * Any flags to forcibly consume caption insets are enabled.
      * * Top activity have configuration coupled with insets.
-     * * Task is not resizeable.
+     * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS]
+     * is enabled.
      */
     fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
         Flags.excludeCaptionFromAppBounds()
                 && isAnyForceConsumptionFlagsEnabled()
                 && taskInfo.topActivityInfo?.let {
-            isInsetsCoupledWithConfiguration(it) && !taskInfo.isResizeable
+            isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled(
+                OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
+            ))
         } ?: false
 
     /**
@@ -81,10 +85,11 @@
     private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage
 
     /**
-     * Returns true if the tasks base activity is part of the default home package.
+     * Returns true if the tasks base activity is part of the default home package, or there is
+     * currently no default home package available.
      */
-    private fun isPartOfDefaultHomePackage(packageName: String?) =
-        packageName != null && packageName == defaultHomePackage
+    private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) =
+        defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage)
 
     private fun isAnyForceConsumptionFlagsEnabled(): Boolean =
         DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 2586bd6..643c150 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -220,6 +220,13 @@
     }
 
     /**
+     * Return {@code true} if the current device can host desktop sessions on its internal display.
+     */
+    public static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+        return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
+    }
+
+    /**
      * Return {@code true} if desktop mode dev option should be shown on current device
      */
     public static boolean canShowDesktopModeDevOption(@NonNull Context context) {
@@ -231,21 +238,24 @@
      * Return {@code true} if desktop mode dev option should be shown on current device
      */
     public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
-        return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context);
+        return Flags.showDesktopExperienceDevOption()
+                && isInternalDisplayEligibleToHostDesktops(context);
     }
 
     /** Returns if desktop mode dev option should be enabled if there is no user override. */
     public static boolean shouldDevOptionBeEnabledByDefault(Context context) {
-        return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode();
+        return isInternalDisplayEligibleToHostDesktops(context)
+                && Flags.enableDesktopWindowingMode();
     }
 
     /**
      * Return {@code true} if desktop mode is enabled and can be entered on the current device.
      */
     public static boolean canEnterDesktopMode(@NonNull Context context) {
-        return (isDeviceEligibleForDesktopMode(context)
-                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
-                || isDesktopModeEnabledByDevOption(context);
+        return (isInternalDisplayEligibleToHostDesktops(context)
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
+                && (isDesktopModeSupported(context) || !enforceDeviceRestrictions())
+                || isDesktopModeEnabledByDevOption(context));
     }
 
     /**
@@ -313,10 +323,11 @@
     }
 
     /**
-     * Return {@code true} if desktop mode is unrestricted and is supported in the device.
+     * Return {@code true} if desktop sessions is unrestricted and can be host for the device's
+     * internal display.
      */
-    public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
-        return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || (
+    public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
+        return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
                 Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported(
                         context));
     }
@@ -325,7 +336,7 @@
      * Return {@code true} if the developer option for desktop mode is unrestricted and is supported
      * in the device.
      *
-     * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then
+     * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then
      * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true.
      */
     private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
index 4127adc..12938db 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
@@ -24,4 +24,9 @@
      * ignore drag events.
      */
     public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION";
+
+    /**
+     * An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo}
+     */
+    public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO";
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index ddcdf9f..d948928 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -54,6 +54,7 @@
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.common.ComponentUtils;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.bubbles.BubbleInfo;
@@ -282,6 +283,29 @@
         mPackageName = intent.getPackage();
     }
 
+    private Bubble(
+            PendingIntent intent,
+            UserHandle user,
+            String key,
+            @ShellMainThread Executor mainExecutor,
+            @ShellBackgroundThread Executor bgExecutor) {
+        mGroupKey = null;
+        mLocusId = null;
+        mFlags = 0;
+        mUser = user;
+        mIcon = null;
+        mType = BubbleType.TYPE_APP;
+        mKey = key;
+        mShowBubbleUpdateDot = false;
+        mMainExecutor = mainExecutor;
+        mBgExecutor = bgExecutor;
+        mTaskId = INVALID_TASK_ID;
+        mPendingIntent = intent;
+        mIntent = null;
+        mDesiredHeight = Integer.MAX_VALUE;
+        mPackageName = ComponentUtils.getPackageName(intent);
+    }
+
     private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor,
             @ShellBackgroundThread Executor bgExecutor) {
         mGroupKey = null;
@@ -336,6 +360,15 @@
     }
 
     /** Creates an app bubble. */
+    public static Bubble createAppBubble(PendingIntent intent, UserHandle user,
+            @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
+        return new Bubble(intent,
+                user,
+                /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
+                mainExecutor, bgExecutor);
+    }
+
+    /** Creates an app bubble. */
     public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
         return new Bubble(intent,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index b93b7b8..c7a0401 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -46,6 +46,7 @@
 import android.app.PendingIntent;
 import android.app.TaskInfo;
 import android.content.BroadcastReceiver;
+import android.content.ClipDescription;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -118,6 +119,7 @@
 import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
 import com.android.wm.shell.shared.bubbles.DeviceConfig;
+import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
@@ -791,15 +793,21 @@
     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
             @BubbleBarLocation.UpdateSource int source) {
         if (isShowingAsBubbleBar()) {
+            updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source);
+            BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+            bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
+            mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
+        }
+    }
+
+    private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
+            @BubbleBarLocation.UpdateSource int source) {
+        if (isShowingAsBubbleBar()) {
             BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation();
             mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
             if (mLayerView != null && !mLayerView.isExpandedViewDragged()) {
                 mLayerView.updateExpandedView();
             }
-            BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
-            bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
-            mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
-
             logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source);
         }
     }
@@ -872,11 +880,20 @@
     }
 
     @Override
-    public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent appIntent,
-            UserHandle userHandle) {
-        if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
-            hideBubbleBarExpandedViewDropTarget();
-            expandStackAndSelectBubble(appIntent, userHandle, location);
+    public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location,
+            Intent itemIntent) {
+        hideBubbleBarExpandedViewDropTarget();
+        ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent
+                .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO);
+        if (shortcutInfo != null) {
+            expandStackAndSelectBubble(shortcutInfo, location);
+            return;
+        }
+        UserHandle user = (UserHandle) itemIntent.getExtra(Intent.EXTRA_USER);
+        PendingIntent pendingIntent = (PendingIntent) itemIntent
+                .getExtra(ClipDescription.EXTRA_PENDING_INTENT);
+        if (pendingIntent != null && user != null) {
+            expandStackAndSelectBubble(pendingIntent, user, location);
         }
     }
 
@@ -1506,12 +1523,37 @@
      * Expands and selects a bubble created or found via the provided shortcut info.
      *
      * @param info the shortcut info for the bubble.
+     * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
      */
-    public void expandStackAndSelectBubble(ShortcutInfo info) {
+    public void expandStackAndSelectBubble(ShortcutInfo info,
+            @Nullable BubbleBarLocation bubbleBarLocation) {
         if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+        BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null;
+        if (updateLocation != null) {
+            updateExpandedViewForBubbleBarLocation(updateLocation,
+                    BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
+        }
         Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow
         ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info);
         if (b.isInflated()) {
+            mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation);
+        } else {
+            b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+            inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false,
+                    updateLocation);
+        }
+    }
+
+    /**
+     * Expands and selects a bubble created or found for this app.
+     *
+     * @param intent the intent for the bubble.
+     */
+    public void expandStackAndSelectBubble(Intent intent, UserHandle user) {
+        if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+        Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
+        ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
+        if (b.isInflated()) {
             mBubbleData.setSelectedBubbleAndExpandStack(b);
         } else {
             b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
@@ -1522,23 +1564,25 @@
     /**
      * Expands and selects a bubble created or found for this app.
      *
-     * @param intent the intent for the bubble.
+     * @param pendingIntent     the intent for the bubble.
+     * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
      */
-    public void expandStackAndSelectBubble(Intent intent, UserHandle user,
+    public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user,
             @Nullable BubbleBarLocation bubbleBarLocation) {
         if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
-        if (bubbleBarLocation != null) {
-            //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack &
-            // fix bubble bar flicking
-            setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
+        BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null;
+        if (updateLocation != null) {
+            updateExpandedViewForBubbleBarLocation(updateLocation,
+                    BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
         }
-        Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
-        ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
+        Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user);
+        ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s",
+                pendingIntent);
         if (b.isInflated()) {
-            mBubbleData.setSelectedBubbleAndExpandStack(b);
+            mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation);
         } else {
             b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
-            inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+            inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation);
         }
     }
 
@@ -1904,11 +1948,22 @@
 
     @VisibleForTesting
     public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+        inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null);
+    }
+
+    /**
+     * Inflates and adds a bubble. Updates Bubble Bar location if bubbles
+     * are shown in the Bubble Bar and the location is not null.
+     */
+    @VisibleForTesting
+    public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade,
+            @Nullable BubbleBarLocation bubbleBarLocation) {
         // Lazy init stack view when a bubble is created
         ensureBubbleViewsAndWindowCreated();
         bubble.setInflateSynchronously(mInflateSynchronously);
         bubble.inflate(
-                b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
+                b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade,
+                        bubbleBarLocation),
                 mContext,
                 mExpandedViewManager,
                 mBubbleTaskViewFactory,
@@ -2242,7 +2297,8 @@
             ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
                     + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
                     + " expanded=%b selectionChanged=%b selected=%s"
-                    + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
+                    + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b"
+                    + " bubbleBarLocation=%s",
                     update.addedBubble != null ? update.addedBubble.getKey() : "null",
                     !update.removedBubbles.isEmpty(),
                     update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
@@ -2251,7 +2307,9 @@
                     update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
                     update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
                     update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
-                    update.shouldShowEducation, update.showOverflowChanged);
+                    update.shouldShowEducation, update.showOverflowChanged,
+                    update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString()
+                            : "null");
 
             ensureBubbleViewsAndWindowCreated();
 
@@ -2756,13 +2814,13 @@
 
         @Override
         public void showShortcutBubble(ShortcutInfo info) {
-            mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info));
+            mMainExecutor.execute(() -> mController
+                    .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null));
         }
 
         @Override
         public void showAppBubble(Intent intent, UserHandle user) {
-            mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent,
-                    user, /* bubbleBarLocation = */ null));
+            mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user));
         }
 
         @Override
@@ -2983,9 +3041,10 @@
 
         @Override
         public void expandStackAndSelectBubble(ShortcutInfo info) {
-            mMainExecutor.execute(() -> {
-                BubbleController.this.expandStackAndSelectBubble(info);
-            });
+            mMainExecutor.execute(() ->
+                    BubbleController.this
+                            .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null)
+            );
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 96d0f6d..abcdb7e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.bubbles.Bubbles.DismissReason;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.shared.bubbles.RemovedBubble;
 
@@ -91,6 +92,8 @@
         @Nullable Bubble suppressedBubble;
         @Nullable Bubble unsuppressedBubble;
         @Nullable String suppressedSummaryGroup;
+        @Nullable
+        BubbleBarLocation mBubbleBarLocation;
         // Pair with Bubble and @DismissReason Integer
         final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
 
@@ -116,6 +119,7 @@
                     || unsuppressedBubble != null
                     || suppressedSummaryChanged
                     || suppressedSummaryGroup != null
+                    || mBubbleBarLocation != null
                     || showOverflowChanged;
         }
 
@@ -169,6 +173,7 @@
             }
             bubbleBarUpdate.showOverflowChanged = showOverflowChanged;
             bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty();
+            bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation;
             return bubbleBarUpdate;
         }
 
@@ -396,8 +401,23 @@
      * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates.
      */
     public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) {
+        setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null);
+    }
+
+    /**
+     * Sets the selected bubble and expands it. Also updates bubble bar location if the
+     * bubbleBarLocation is not {@code null}
+     *
+     * <p>This dispatches a single state update for 3 changes and should be used instead of
+     * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by
+     * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will
+     * generate 2 separate updates.
+     */
+    public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble,
+            @Nullable BubbleBarLocation bubbleBarLocation) {
         setSelectedBubbleInternal(bubble);
         setExpandedInternal(true);
+        mStateChange.mBubbleBarLocation = bubbleBarLocation;
         dispatchPendingChanges();
     }
 
@@ -471,6 +491,16 @@
         return bubbleToReturn;
     }
 
+    Bubble getOrCreateBubble(PendingIntent pendingIntent, UserHandle user) {
+        String bubbleKey = Bubble.getAppBubbleKeyForApp(pendingIntent.getCreatorPackage(), user);
+        Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+        if (bubbleToReturn == null) {
+            bubbleToReturn = Bubble.createAppBubble(pendingIntent, user, mMainExecutor,
+                    mBgExecutor);
+        }
+        return bubbleToReturn;
+    }
+
     Bubble getOrCreateBubble(TaskInfo taskInfo) {
         UserHandle user = UserHandle.of(mCurrentUserId);
         String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo);
@@ -503,13 +533,25 @@
     }
 
     /**
+     * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing
+     * {@code null} for bubbleBarLocation.
+     *
+     * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)
+     */
+    void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+        notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */
+                null);
+    }
+
+    /**
      * When this method is called it is expected that all info in the bubble has completed loading.
      * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager,
      * BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView,
      * com.android.wm.shell.bubbles.bar.BubbleBarLayerView,
      * com.android.launcher3.icons.BubbleIconFactory, boolean)
      */
-    void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+    void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade,
+            @Nullable BubbleBarLocation bubbleBarLocation) {
         mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
         Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
         suppressFlyout |= !bubble.isTextChanged();
@@ -557,6 +599,7 @@
                 doSuppress(bubble);
             }
         }
+        mStateChange.mBubbleBarLocation = bubbleBarLocation;
         dispatchPendingChanges();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 4a0eee86..e47ac61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -117,15 +117,24 @@
                         Context context =
                                 mContext.createContextAsUser(
                                         mBubble.getUser(), Context.CONTEXT_RESTRICTED);
-                        PendingIntent pi = PendingIntent.getActivity(
-                                context,
-                                /* requestCode= */ 0,
-                                mBubble.getIntent()
-                                        .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
-                                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
-                                /* options= */ null);
-                        mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
-                                launchBounds);
+                        Intent fillInIntent = null;
+                        //first try get pending intent from the bubble
+                        PendingIntent pi = mBubble.getPendingIntent();
+                        if (pi == null) {
+                            // if null - create new one
+                            pi = PendingIntent.getActivity(
+                                    context,
+                                    /* requestCode= */ 0,
+                                    mBubble.getIntent()
+                                            .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+                                    PendingIntent.FLAG_IMMUTABLE
+                                            | PendingIntent.FLAG_UPDATE_CURRENT,
+                                    /* options= */ null);
+                        } else {
+                            fillInIntent = new Intent(pi.getIntent());
+                            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                        }
+                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
                     } else if (isShortcutBubble) {
                         options.setLaunchedFromBubble(true);
                         options.setApplyActivityFlagsForBubbles(true);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
index afe5c87..3ff80b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
@@ -18,7 +18,6 @@
 
 import android.content.Intent
 import android.graphics.Rect
-import android.os.UserHandle
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 /** Controller that takes care of the bubble bar drag events. */
@@ -31,11 +30,7 @@
     fun onItemDraggedOutsideBubbleBarDropZone()
 
     /** Called when the drop event happens over the bubble bar drop zone. */
-    fun onItemDroppedOverBubbleBarDragZone(
-        location: BubbleBarLocation,
-        intent: Intent,
-        userHandle: UserHandle
-    )
+    fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation, itemIntent: Intent)
 
     /**
      * Returns mapping of the bubble bar locations to the corresponding
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index aa42de6..e3b0872 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -524,8 +524,8 @@
      * Skips logging if it is {@link BubbleOverflow}.
      */
     private void logBubbleEvent(BubbleLogger.Event event) {
-        if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) {
-            mBubbleLogger.log(bubble, event);
+        if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) {
+            mBubbleLogger.log((Bubble) mExpandedBubble, event);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b2b99d6..5de49b7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -914,12 +914,15 @@
             Context context,
             Transitions transitions,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            @DynamicOverride DesktopUserRepositories desktopUserRepositories,
             InteractionJankMonitor interactionJankMonitor) {
         return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue()
                 ? new SpringDragToDesktopTransitionHandler(
-                        context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor)
+                context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories,
+                interactionJankMonitor)
                 : new DefaultDragToDesktopTransitionHandler(
-                        context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor);
+                        context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories,
+                        interactionJankMonitor);
     }
 
     @WMSingleton
@@ -1303,7 +1306,8 @@
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             DesktopWindowingEducationTooltipController desktopWindowingEducationTooltipController,
             @ShellMainThread CoroutineScope applicationScope,
-            @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher) {
+            @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher,
+            DesktopModeUiEventLogger desktopModeUiEventLogger) {
         return new AppHandleEducationController(
                 context,
                 appHandleEducationFilter,
@@ -1311,7 +1315,8 @@
                 windowDecorCaptionHandleRepository,
                 desktopWindowingEducationTooltipController,
                 applicationScope,
-                backgroundDispatcher);
+                backgroundDispatcher,
+                desktopModeUiEventLogger);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
index b96b9d2..b9cb32d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
@@ -149,7 +149,25 @@
         @UiEvent(doc = "Enter multi-instance by using the New Window button")
         DESKTOP_WINDOW_MULTI_INSTANCE_NEW_WINDOW_CLICK(2069),
         @UiEvent(doc = "Enter multi-instance by clicking an icon in the Manage Windows menu")
-        DESKTOP_WINDOW_MULTI_INSTANCE_MANAGE_WINDOWS_ICON_CLICK(2070);
+        DESKTOP_WINDOW_MULTI_INSTANCE_MANAGE_WINDOWS_ICON_CLICK(2070),
+        @UiEvent(doc = "Education tooltip on the app handle is shown")
+        APP_HANDLE_EDUCATION_TOOLTIP_SHOWN(2097),
+        @UiEvent(doc = "Education tooltip on the app handle is clicked")
+        APP_HANDLE_EDUCATION_TOOLTIP_CLICKED(2098),
+        @UiEvent(doc = "Education tooltip on the app handle is dismissed by the user")
+        APP_HANDLE_EDUCATION_TOOLTIP_DISMISSED(2099),
+        @UiEvent(doc = "Enter desktop mode education tooltip on the app handle menu is shown")
+        ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN(2100),
+        @UiEvent(doc = "Enter desktop mode education tooltip on the app handle menu is clicked")
+        ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED(2101),
+        @UiEvent(doc = "Enter desktop mode education tooltip is dismissed by the user")
+        ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2102),
+        @UiEvent(doc = "Exit desktop mode education tooltip on the app header menu is shown")
+        EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN(2103),
+        @UiEvent(doc = "Exit desktop mode education tooltip on the app header menu is clicked")
+        EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED(2104),
+        @UiEvent(doc = "Exit desktop mode education tooltip is dismissed by the user")
+        EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2105);
 
         override fun getId(): Int = mId
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 3c77807..531304d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -343,10 +343,22 @@
             DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
                 .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
         ) {
+            logV(
+                "isDesktopModeShowing: hasVisibleTasks=%s hasTopTransparentFullscreenTask=%s hasMinimizedPip=%s",
+                hasVisibleTasks,
+                hasTopTransparentFullscreenTask,
+                hasMinimizedPip,
+            )
             return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip
         } else if (Flags.enableDesktopWindowingPip()) {
+            logV(
+                "isDesktopModeShowing: hasVisibleTasks=%s hasMinimizedPip=%s",
+                hasVisibleTasks,
+                hasMinimizedPip,
+            )
             return hasVisibleTasks || hasMinimizedPip
         }
+        logV("isDesktopModeShowing: hasVisibleTasks=%s", hasVisibleTasks)
         return hasVisibleTasks
     }
 
@@ -1647,11 +1659,16 @@
     private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
         logV("addWallpaperActivity")
         if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
+
+            // If the wallpaper activity for this display already exists, let's reorder it to top.
+            val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId)
+            if (wallpaperActivityToken != null) {
+                wct.reorder(wallpaperActivityToken, /* onTop= */ true)
+                return
+            }
+
             val intent = Intent(context, DesktopWallpaperActivity::class.java)
-            if (
-                desktopWallpaperActivityTokenProvider.getToken(displayId) == null &&
-                    Flags.enablePerDisplayDesktopWallpaperActivity()
-            ) {
+            if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
             }
@@ -3074,6 +3091,7 @@
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
                 pendingIntentLaunchFlags =
                     Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+                splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
             }
         if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
             dragAndDropFullscreenCookie = Binder()
@@ -3082,7 +3100,12 @@
         val wct = WindowContainerTransaction()
         wct.sendPendingIntent(launchIntent, null, opts.toBundle())
         if (windowingMode == WINDOWING_MODE_FREEFORM) {
-            desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+            if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) {
+                // TODO b/376389593: Use a custom tab tearing transition/animation
+                startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+            } else {
+                desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+            }
         } else {
             transitions.startTransition(TRANSIT_OPEN, wct, null)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
index a5ba661..c10752d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
@@ -90,6 +90,11 @@
         return desktopRepoByUserId.getOrCreate(profileId)
     }
 
+    fun getUserIdForProfile(profileId: Int): Int {
+        if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId
+        else return profileId
+    }
+
     /** Dumps [DesktopRepository] for each user. */
     fun dump(pw: PrintWriter, prefix: String) {
         desktopRepoByUserId.forEach { key, value ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 2ac76f3..8194d3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -70,6 +70,7 @@
     private val context: Context,
     private val transitions: Transitions,
     private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    private val desktopUserRepositories: DesktopUserRepositories,
     protected val interactionJankMonitor: InteractionJankMonitor,
     protected val transactionSupplier: Supplier<SurfaceControl.Transaction>,
 ) : TransitionHandler {
@@ -127,15 +128,18 @@
                 pendingIntentCreatorBackgroundActivityStartMode =
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
             }
-        val taskUser = UserHandle.of(taskInfo.userId)
+        // If we are launching home for a profile of a user, just use the [userId] of that user
+        // instead of the [profileId] to create the context.
+        val userToLaunchWith =
+            UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId))
         val pendingIntent =
             PendingIntent.getActivityAsUser(
-                context.createContextAsUser(taskUser, /* flags= */ 0),
+                context.createContextAsUser(userToLaunchWith, /* flags= */ 0),
                 /* requestCode= */ 0,
                 launchHomeIntent,
                 FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
                 options.toBundle(),
-                taskUser,
+                userToLaunchWith,
             )
         val wct = WindowContainerTransaction()
         // The app that is being dragged into desktop mode might cause new transitions, make this
@@ -881,6 +885,7 @@
     context: Context,
     transitions: Transitions,
     taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    desktopUserRepositories: DesktopUserRepositories,
     interactionJankMonitor: InteractionJankMonitor,
     transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
         SurfaceControl.Transaction()
@@ -890,6 +895,7 @@
         context,
         transitions,
         taskDisplayAreaOrganizer,
+        desktopUserRepositories,
         interactionJankMonitor,
         transactionSupplier,
     ) {
@@ -917,6 +923,7 @@
     context: Context,
     transitions: Transitions,
     taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    desktopUserRepositories: DesktopUserRepositories,
     interactionJankMonitor: InteractionJankMonitor,
     transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
         SurfaceControl.Transaction()
@@ -926,6 +933,7 @@
         context,
         transitions,
         taskDisplayAreaOrganizer,
+        desktopUserRepositories,
         interactionJankMonitor,
         transactionSupplier,
     ) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index 5d83556..f664514 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -26,6 +26,8 @@
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.desktopmode.CaptionState
+import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger
+import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
 import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread
@@ -62,6 +64,7 @@
     private val windowingEducationViewController: DesktopWindowingEducationTooltipController,
     @ShellMainThread private val applicationCoroutineScope: CoroutineScope,
     @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher,
+    private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
 ) {
     private lateinit var openHandleMenuCallback: (Int) -> Unit
     private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit
@@ -171,6 +174,7 @@
 
     private fun showEducation(captionState: CaptionState) {
         val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds
+        val taskInfo = captionState.runningTaskInfo
         val tooltipGlobalCoordinates =
             Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom)
         // Populate information important to inflate app handle education tooltip.
@@ -188,22 +192,34 @@
                 arrowDirection =
                     DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP,
                 onEducationClickAction = {
-                    openHandleMenuCallback(captionState.runningTaskInfo.taskId)
+                    openHandleMenuCallback(taskInfo.taskId)
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_CLICKED,
+                    )
                 },
                 onDismissAction = {
-                    // TODO: b/341320146 - Log previous tooltip was dismissed
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_DISMISSED,
+                    )
                 },
             )
 
         windowingEducationViewController.showEducationTooltip(
             tooltipViewConfig = appHandleTooltipConfig,
-            taskId = captionState.runningTaskInfo.taskId,
+            taskId = taskInfo.taskId,
+        )
+        desktopModeUiEventLogger.log(
+            taskInfo,
+            DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_SHOWN,
         )
     }
 
     /** Show tooltip that points to windowing image button in app handle menu */
     private suspend fun showWindowingImageButtonTooltip(captionState: CaptionState.AppHandle) {
         val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height)
+        val taskInfo = captionState.runningTaskInfo
         val windowingOptionPillHeight =
             getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height)
         val appHandleMenuWidth =
@@ -245,24 +261,36 @@
                     DesktopWindowingEducationTooltipController.TooltipArrowDirection.HORIZONTAL,
                 onEducationClickAction = {
                     toDesktopModeCallback(
-                        captionState.runningTaskInfo.taskId,
+                        taskInfo.taskId,
                         DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
                     )
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED,
+                    )
                 },
                 onDismissAction = {
-                    // TODO: b/341320146 - Log previous tooltip was dismissed
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED,
+                    )
                 },
             )
 
         windowingEducationViewController.showEducationTooltip(
-            taskId = captionState.runningTaskInfo.taskId,
+            taskId = taskInfo.taskId,
             tooltipViewConfig = windowingImageButtonTooltipConfig,
         )
+        desktopModeUiEventLogger.log(
+            taskInfo,
+            DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN,
+        )
     }
 
     /** Show tooltip that points to app chip button and educates user on how to exit desktop mode */
     private suspend fun showExitWindowingTooltip(captionState: CaptionState.AppHeader) {
         val globalAppChipBounds = captionState.globalAppChipBounds
+        val taskInfo = captionState.runningTaskInfo
         val tooltipGlobalCoordinates =
             Point(
                 if (isRtl()) {
@@ -287,16 +315,27 @@
                 arrowDirection =
                     DesktopWindowingEducationTooltipController.TooltipArrowDirection.HORIZONTAL,
                 onDismissAction = {
-                    // TODO: b/341320146 - Log previous tooltip was dismissed
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED,
+                    )
                 },
                 onEducationClickAction = {
-                    openHandleMenuCallback(captionState.runningTaskInfo.taskId)
+                    openHandleMenuCallback(taskInfo.taskId)
+                    desktopModeUiEventLogger.log(
+                        taskInfo,
+                        DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED,
+                    )
                 },
             )
         windowingEducationViewController.showEducationTooltip(
-            taskId = captionState.runningTaskInfo.taskId,
+            taskId = taskInfo.taskId,
             tooltipViewConfig = exitWindowingTooltipConfig,
         )
+        desktopModeUiEventLogger.log(
+            taskInfo,
+            DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN,
+        )
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 2571e0e..b3c1a92 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -47,7 +47,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
 import android.view.DragEvent;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -70,6 +69,7 @@
 import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
@@ -627,8 +627,7 @@
     @Nullable
     private BubbleBarLocation getBubbleBarLocation(int x, int y) {
         Intent appData = mSession.appData;
-        if (appData == null || appData.getExtra(Intent.EXTRA_INTENT) == null
-                || appData.getExtra(Intent.EXTRA_USER) == null) {
+        if (appData == null) {
             // there is no app data, so drop event over the bubble bar can not be handled
             return null;
         }
@@ -686,11 +685,10 @@
         // Process the drop exclusive by DropTarget OR by the BubbleBar
         if (mCurrentTarget != null) {
             mPolicy.onDropped(mCurrentTarget, hideTaskToken);
-        } else if (appData != null && mCurrentBubbleBarTarget != null) {
-            Intent appIntent = (Intent) appData.getExtra(Intent.EXTRA_INTENT);
-            UserHandle user = (UserHandle) appData.getExtra(Intent.EXTRA_USER);
+        } else if (appData != null && mCurrentBubbleBarTarget != null
+                && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
             mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget,
-                    appIntent, user);
+                    appData);
         }
 
         // Start animating the drop UI out with the drag surface
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
index b4cf890..88ac865 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
@@ -26,6 +26,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.hardware.HardwareBuffer;
 import android.util.TypedValue;
 import android.view.SurfaceControl;
 
@@ -39,7 +40,6 @@
 
     private final Context mContext;
     private final int mAppIconSizePx;
-    private final Rect mAppBounds;
     private final int mOverlayHalfSize;
     private final Matrix mTmpTransform = new Matrix();
     private final float[] mTmpFloat9 = new float[9];
@@ -56,10 +56,6 @@
         final int overlaySize = getOverlaySize(appBounds, destinationBounds);
         mOverlayHalfSize = overlaySize >> 1;
 
-        // When the activity is in the secondary split, make sure the scaling center is not
-        // offset.
-        mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height());
-
         mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
         prepareAppIconOverlay(appIcon);
         mLeash = new SurfaceControl.Builder()
@@ -85,12 +81,17 @@
 
     @Override
     public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+        final HardwareBuffer buffer = mBitmap.getHardwareBuffer();
         tx.show(mLeash);
         tx.setLayer(mLeash, Integer.MAX_VALUE);
-        tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+        tx.setBuffer(mLeash, buffer);
         tx.setAlpha(mLeash, 0f);
         tx.reparent(mLeash, parentLeash);
         tx.apply();
+        // Cleanup the bitmap and buffer after setting up the leash
+        mBitmap.recycle();
+        mBitmap = null;
+        buffer.close();
     }
 
     @Override
@@ -108,16 +109,6 @@
                 .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
     }
 
-
-
-    @Override
-    public void detach(SurfaceControl.Transaction tx) {
-        super.detach(tx);
-        if (mBitmap != null && !mBitmap.isRecycled()) {
-            mBitmap.recycle();
-        }
-    }
-
     private void prepareAppIconOverlay(Drawable appIcon) {
         final Canvas canvas = new Canvas();
         canvas.setBitmap(mBitmap);
@@ -139,6 +130,8 @@
                 mOverlayHalfSize + mAppIconSizePx / 2);
         appIcon.setBounds(appIconBounds);
         appIcon.draw(canvas);
+        Bitmap oldBitmap = mBitmap;
         mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+        oldBitmap.recycle();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index bb9b479..a57b4b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -72,7 +72,6 @@
 import com.android.wm.shell.pip2.animation.PipEnterAnimator;
 import com.android.wm.shell.pip2.animation.PipExpandAnimator;
 import com.android.wm.shell.shared.TransitionUtil;
-import com.android.wm.shell.shared.pip.PipContentOverlay;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -422,7 +421,7 @@
         final Rect destinationBounds = pipChange.getEndAbsBounds();
         final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay();
         if (swipePipToHomeOverlay != null) {
-            final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize(
+            final int overlaySize = PipAppIconOverlay.getOverlaySize(
                     mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds);
             // It is possible we reparent the PIP activity to a new PIP task (in multi-activity
             // apps), so we should also reparent the overlay to the final PIP task.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 8ad2e1d..7751741 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -29,6 +29,7 @@
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_SLEEP;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
@@ -46,6 +47,7 @@
 import android.app.ActivityTaskManager;
 import android.app.IApplicationThread;
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -73,6 +75,7 @@
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.Flags;
+import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipUtils;
@@ -317,7 +320,7 @@
                     "RecentsTransitionHandler.mergeAnimation: no controller found");
             return;
         }
-        controller.merge(info, startT, mergeTarget, finishCallback);
+        controller.merge(info, startT, finishT, mergeTarget, finishCallback);
     }
 
     @Override
@@ -912,7 +915,8 @@
          * before any unhandled transitions.
          */
         @SuppressLint("NewApi")
-        void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget,
+        void merge(TransitionInfo info, SurfaceControl.Transaction startT,
+                SurfaceControl.Transaction finishT, IBinder mergeTarget,
                 Transitions.TransitionFinishCallback finishCallback) {
             if (mFinishCB == null) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -1072,8 +1076,8 @@
                     Slog.e(TAG, "Returning to recents without closing any opening tasks.");
                 }
                 // Setup may hide it initially since it doesn't know that overview was still active.
-                t.show(recentsOpening.getLeash());
-                t.setAlpha(recentsOpening.getLeash(), 1.f);
+                startT.show(recentsOpening.getLeash());
+                startT.setAlpha(recentsOpening.getLeash(), 1.f);
                 mState = STATE_NORMAL;
             }
             boolean didMergeThings = false;
@@ -1142,31 +1146,31 @@
                         mOpeningTasks.add(pausingTask);
                         // Setup hides opening tasks initially, so make it visible again (since we
                         // are already showing it).
-                        t.show(change.getLeash());
-                        t.setAlpha(change.getLeash(), 1.f);
+                        startT.show(change.getLeash());
+                        startT.setAlpha(change.getLeash(), 1.f);
                     } else if (isLeaf) {
                         // We are receiving new opening leaf tasks, so convert to onTasksAppeared.
                         final RemoteAnimationTarget target = TransitionUtil.newTarget(
-                                change, layer, info, t, mLeashMap);
+                                change, layer, info, startT, mLeashMap);
                         appearedTargets[nextTargetIdx++] = target;
                         // reparent into the original `mInfo` since that's where we are animating.
                         final TransitionInfo.Root root = TransitionUtil.getRootFor(change, mInfo);
                         final boolean wasClosing = closingIdx >= 0;
-                        t.reparent(target.leash, root.getLeash());
-                        t.setPosition(target.leash,
+                        startT.reparent(target.leash, root.getLeash());
+                        startT.setPosition(target.leash,
                                 change.getStartAbsBounds().left - root.getOffset().x,
                                 change.getStartAbsBounds().top - root.getOffset().y);
-                        t.setLayer(target.leash, layer);
+                        startT.setLayer(target.leash, layer);
                         if (wasClosing) {
                             // App was previously visible and is closing
-                            t.show(target.leash);
-                            t.setAlpha(target.leash, 1f);
+                            startT.show(target.leash);
+                            startT.setAlpha(target.leash, 1f);
                             // Also override the task alpha as it was set earlier when dispatching
                             // the transition and setting up the leash to hide the
-                            t.setAlpha(change.getLeash(), 1f);
+                            startT.setAlpha(change.getLeash(), 1f);
                         } else {
                             // Hide the animation leash, let the listener show it
-                            t.hide(target.leash);
+                            startT.hide(target.leash);
                         }
                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                                 "  opening new leaf taskId=%d wasClosing=%b",
@@ -1175,10 +1179,10 @@
                     } else {
                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                                 "  opening new taskId=%d", change.getTaskInfo().taskId);
-                        t.setLayer(change.getLeash(), layer);
+                        startT.setLayer(change.getLeash(), layer);
                         // Setup hides opening tasks initially, so make it visible since recents
                         // is only animating the leafs.
-                        t.show(change.getLeash());
+                        startT.show(change.getLeash());
                         mOpeningTasks.add(new TaskState(change, null));
                     }
                 }
@@ -1194,7 +1198,7 @@
                 // Activity only transition, so consume the merge as it doesn't affect the rest of
                 // recents.
                 Slog.d(TAG, "Got an activity only transition during recents, so apply directly");
-                mergeActivityOnly(info, t);
+                mergeActivityOnly(info, startT);
             } else if (!didMergeThings) {
                 // Didn't recognize anything in incoming transition so don't merge it.
                 Slog.w(TAG, "Don't know how to merge this transition, foundRecentsClosing="
@@ -1206,7 +1210,10 @@
                 return;
             }
             // At this point, we are accepting the merge.
-            t.apply();
+            startT.apply();
+            // Since we're accepting the merge, update the finish transaction so that changes via
+            // that transaction will be applied on top of those of the merged transitions
+            mFinishTransaction = finishT;
             // not using the incoming anim-only surfaces
             info.releaseAnimSurfaces();
             if (appearedTargets != null) {
@@ -1349,6 +1356,8 @@
                     wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */);
                     t.show(mPausingTasks.get(i).mTaskSurface);
                 }
+                setCornerRadiusForFreeformTasks(
+                        mRecentTasksController.getContext(), t, mPausingTasks);
                 if (!mKeyguardLocked && mRecentsTask != null) {
                     wct.restoreTransientOrder(mRecentsTask);
                 }
@@ -1386,6 +1395,8 @@
                 for (int i = 0; i < mOpeningTasks.size(); ++i) {
                     t.show(mOpeningTasks.get(i).mTaskSurface);
                 }
+                setCornerRadiusForFreeformTasks(
+                        mRecentTasksController.getContext(), t, mOpeningTasks);
                 for (int i = 0; i < mPausingTasks.size(); ++i) {
                     cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint);
                 }
@@ -1446,6 +1457,11 @@
                             wct.clear();
 
                             if (Flags.enableRecentsBookendTransition()) {
+                                // Notify the mixers of the pending finish
+                                for (int i = 0; i < mMixers.size(); ++i) {
+                                    mMixers.get(i).handleFinishRecents(returningToApp, wct, t);
+                                }
+
                                 // In this case, we've already started the PIP transition, so we can
                                 // clean up immediately
                                 mPendingRunnerFinishCb = runnerFinishCb;
@@ -1505,6 +1521,27 @@
             }
         }
 
+        private static void setCornerRadiusForFreeformTasks(
+                Context context,
+                SurfaceControl.Transaction t,
+                ArrayList<TaskState> tasks) {
+            if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) {
+                return;
+            }
+            int cornerRadius = getCornerRadius(context);
+            for (int i = 0; i < tasks.size(); ++i) {
+                TaskState task = tasks.get(i);
+                if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) {
+                    t.setCornerRadius(task.mTaskSurface, cornerRadius);
+                }
+            }
+        }
+
+        private static int getCornerRadius(Context context) {
+            return context.getResources().getDimensionPixelSize(
+                    R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+        }
+
         private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) {
             if (tasks == null) {
                 return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index aff21cb..a799b7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1675,8 +1675,8 @@
     void prepareExitSplitScreen(@StageType int stageToTop,
             @NonNull WindowContainerTransaction wct, @ExitReason int exitReason) {
         if (!isSplitActive()) return;
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s",
-                stageTypeToString(stageToTop));
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s",
+                stageTypeToString(stageToTop), exitReasonToString(exitReason));
         if (enableFlexibleSplit()) {
             mStageOrderOperator.getActiveStages().stream()
                     .filter(stage -> stage.getId() != stageToTop)
@@ -2859,14 +2859,6 @@
                     prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED);
                     mSplitTransitions.setDismissTransition(transition, dismissTop,
                             EXIT_REASON_APP_FINISHED);
-                } else if (isOpening && !mPausingTasks.isEmpty()) {
-                    // One of the splitting task is opening while animating the split pair in
-                    // recents, which means to dismiss the split pair to this task.
-                    int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN
-                            ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
-                    prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED);
-                    mSplitTransitions.setDismissTransition(transition, dismissTop,
-                            EXIT_REASON_APP_FINISHED);
                 } else if (!isSplitScreenVisible() && isOpening) {
                     // If split is running in the background and the trigger task is appearing into
                     // split, prepare to enter split screen.
@@ -3395,12 +3387,14 @@
         TransitionInfo.Change sideChild = null;
         StageTaskListener firstAppStage = null;
         StageTaskListener secondAppStage = null;
+        boolean foundPausingTask = false;
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         for (int iC = 0; iC < info.getChanges().size(); ++iC) {
             final TransitionInfo.Change change = info.getChanges().get(iC);
             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
             if (taskInfo == null || !taskInfo.hasParentTask()) continue;
             if (mPausingTasks.contains(taskInfo.taskId)) {
+                foundPausingTask = true;
                 continue;
             }
             StageTaskListener stage = getStageOfTask(taskInfo);
@@ -3443,9 +3437,9 @@
                             prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN);
                             logExit(EXIT_REASON_UNKNOWN);
                         });
-                Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
-                        "launched 2 tasks in split, but didn't receive "
-                        + "2 tasks in transition. Possibly one of them failed to launch"));
+                Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in "
+                        + "split, but didn't receive 2 tasks in transition. Possibly one of them "
+                        + "failed to launch (foundPausingTask=" + foundPausingTask + ")"));
                 if (mRecentTasks.isPresent() && mainChild != null) {
                     mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
                 }
@@ -3800,6 +3794,7 @@
 
     /** Call this when the recents animation canceled during split-screen. */
     public void onRecentsInSplitAnimationCanceled() {
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled");
         mPausingTasks.clear();
         setSplitsVisible(false);
 
@@ -3809,31 +3804,10 @@
         mTaskOrganizer.applyTransaction(wct);
     }
 
-    public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
-            @NonNull WindowContainerTransaction finishWct,
-            @NonNull SurfaceControl.Transaction finishT) {
-        if (!Flags.enableRecentsBookendTransition()) {
-            // The non-bookend recents transition case will be handled by
-            // RecentsMixedTransition wrapping the finish callback and calling
-            // onRecentsInSplitAnimationFinish()
-            return;
-        }
-
-        onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
-    }
-
-    /** Call this when the recents animation during split-screen finishes. */
-    public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct,
-            @NonNull SurfaceControl.Transaction finishT) {
-        if (Flags.enableRecentsBookendTransition()) {
-            // The bookend recents transition case will be handled by
-            // onRecentsInSplitAnimationFinishing above
-            return;
-        }
-
-        // Check if the recent transition is finished by returning to the current
-        // split, so we can restore the divider bar.
-        boolean returnToApp = false;
+    /**
+     * Returns whether the given WCT is reordering any of the split tasks to top.
+     */
+    public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) {
         for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
             final WindowContainerTransaction.HierarchyOp op =
                     finishWct.getHierarchyOps().get(i);
@@ -3848,14 +3822,14 @@
             }
             if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
                     && anyStageContainsContainer) {
-                returnToApp = true;
+                return true;
             }
         }
-        onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
+        return false;
     }
 
-    /** Call this when the recents animation during split-screen finishes. */
-    public void onRecentsInSplitAnimationFinishInner(boolean returnToApp,
+    /** Called when the recents animation during split-screen finishes. */
+    public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
             @NonNull WindowContainerTransaction finishWct,
             @NonNull SurfaceControl.Transaction finishT) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b",
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 0689205..01428e6 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
@@ -39,9 +39,12 @@
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -55,6 +58,7 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.internal.jank.Cuj.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
 import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -101,6 +105,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.ProtoLog;
@@ -144,6 +149,8 @@
 
     private Drawable mEnterpriseThumbnailDrawable;
 
+    final InteractionJankMonitor mInteractionJankMonitor;
+
     private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -161,7 +168,8 @@
             @NonNull TransactionPool transactionPool,
             @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor,
-            @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) {
+            @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            @NonNull InteractionJankMonitor interactionJankMonitor) {
         mDisplayController = displayController;
         mTransactionPool = transactionPool;
         mContext = context;
@@ -173,6 +181,7 @@
         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
         shellInit.addInitCallback(this::onInit, this);
         mRootTDAOrganizer = rootTDAOrganizer;
+        mInteractionJankMonitor = interactionJankMonitor;
     }
 
     private void onInit() {
@@ -321,8 +330,17 @@
         final ArrayList<Animator> animations = new ArrayList<>();
         mAnimations.put(transition, animations);
 
+        final boolean isTaskTransition = isTaskTransition(info);
+        if (isTaskTransition) {
+            mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext,
+                    mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+        }
+
         final Runnable onAnimFinish = () -> {
             if (!animations.isEmpty()) return;
+            if (isTaskTransition) {
+                mInteractionJankMonitor.end(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+            }
             mAnimations.remove(transition);
             finishCallback.onTransitionFinished(null /* wct */);
         };
@@ -678,6 +696,30 @@
     }
 
     /**
+     * A task transition is defined as a transition where there is exaclty one open/to_front task
+     * and one close/to_back task. Nothing else is allowed to be included in the transition
+     */
+    public static boolean isTaskTransition(@NonNull TransitionInfo info) {
+        if (info.getChanges().size() != 2) {
+            return false;
+        }
+        boolean hasOpeningTask = false;
+        boolean hasClosingTask = false;
+
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.getTaskInfo() == null) {
+                // A non-task is in the transition
+                return false;
+            }
+            int mode = change.getMode();
+            hasOpeningTask |= mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
+            hasClosingTask |= mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+        }
+        return hasOpeningTask && hasClosingTask;
+    }
+
+    /**
      * Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select
      * different animations and z-orders for these
      */
@@ -986,4 +1028,10 @@
                 || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS
                 || animType == ANIM_FROM_STYLE;
     }
+
+    @Override
+    public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+                              @Nullable SurfaceControl.Transaction finishTransaction) {
+        mInteractionJankMonitor.cancel(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index f40dc8a..1e926c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -159,9 +159,17 @@
             // If pair-to-pair switching, the post-recents clean-up isn't needed.
             wct = wct != null ? wct : new WindowContainerTransaction();
             if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
-                // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove
-                // once that rolls out
-                mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+                // We've dispatched to the mLeftoversHandler to handle the rest of the transition
+                // and called onRecentsInSplitAnimationStart(), but if the recents handler is not
+                // actually handling the transition, then onRecentsInSplitAnimationFinishing()
+                // won't actually get called by the recents handler.  In such cases, we still need
+                // to clean up after the changes from the start call.
+                boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler;
+                if (!splitNotifiedByRecents) {
+                    mSplitHandler.onRecentsInSplitAnimationFinishing(
+                            mSplitHandler.wctIsReorderingSplitToTop(wct),
+                            wct, finishTransaction);
+                }
             } else {
                 // notify pair-to-pair recents animation finish
                 mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index c90f6cf..deb8839 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -77,6 +77,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -335,7 +336,8 @@
         mDisplayController = displayController;
         mPlayerImpl = new TransitionPlayerImpl();
         mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
-                displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer);
+                displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer,
+                InteractionJankMonitor.getInstance());
         mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index c92e67f..a17bcb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -35,10 +35,8 @@
 import android.view.View
 import android.view.WindowInsets.Type.systemBars
 import android.view.WindowManager
-import android.widget.Button
 import android.widget.ImageButton
 import android.widget.ImageView
-import android.widget.TextView
 import android.window.DesktopModeFlags
 import android.window.SurfaceSyncGroup
 import androidx.annotation.StringRes
@@ -540,17 +538,35 @@
                 return@setOnTouchListener true
             }
 
-            with(context.resources) {
-                // Update a11y read out to say "double tap to enter desktop windowing mode"
+            with(context) {
+                // Update a11y announcement out to say "double tap to enter Fullscreen"
                 ViewCompat.replaceAccessibilityAction(
-                    desktopBtn, ACTION_CLICK,
-                    getString(R.string.app_handle_menu_talkback_desktop_mode_button_text), null
+                    fullscreenBtn, ACTION_CLICK,
+                    getString(
+                        R.string.app_handle_menu_accessibility_announce,
+                        getString(R.string.fullscreen_text)
+                    ),
+                    null,
                 )
 
-                // Update a11y read out to say "double tap to enter split screen mode"
+                // Update a11y announcement out to say "double tap to enter Desktop View"
+                ViewCompat.replaceAccessibilityAction(
+                    desktopBtn, ACTION_CLICK,
+                    getString(
+                        R.string.app_handle_menu_accessibility_announce,
+                        getString(R.string.desktop_text)
+                    ),
+                    null,
+                )
+
+                // Update a11y announcement to say "double tap to enter Split Screen"
                 ViewCompat.replaceAccessibilityAction(
                     splitscreenBtn, ACTION_CLICK,
-                    getString(R.string.app_handle_menu_talkback_split_screen_mode_button_text), null
+                    getString(
+                        R.string.app_handle_menu_accessibility_announce,
+                        getString(R.string.split_screen_text)
+                    ),
+                    null,
                 )
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 4762bc2..90c865e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -93,9 +93,6 @@
     private val lightColors = dynamicLightColorScheme(context)
     private val darkColors = dynamicDarkColorScheme(context)
 
-    private val headerButtonOpenMenuA11yText = context.resources
-        .getString(R.string.desktop_mode_app_header_chip_text)
-
     /**
      * The corner radius to apply to the app chip, maximize and close button's background drawable.
      **/
@@ -231,35 +228,29 @@
             }
         }
 
-        val a11yActionOpenHeaderMenu = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
-            headerButtonOpenMenuA11yText)
-        openMenuButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
-            override fun onInitializeAccessibilityNodeInfo(
-                host: View,
-                info: AccessibilityNodeInfo
-            ) {
-                super.onInitializeAccessibilityNodeInfo(host, info)
-                info.addAction(a11yActionOpenHeaderMenu)
-            }
-        }
+        // Update a11y announcement to say "double tap to open menu"
+        ViewCompat.replaceAccessibilityAction(
+            openMenuButton,
+            AccessibilityActionCompat.ACTION_CLICK,
+            context.getString(R.string.app_handle_chip_accessibility_announce),
+            null
+        )
 
-        with(context.resources) {
-            // Update a11y read out to say "double tap to maximize or restore window size"
-            ViewCompat.replaceAccessibilityAction(
-                maximizeWindowButton,
-                AccessibilityActionCompat.ACTION_CLICK,
-                getString(R.string.maximize_button_talkback_action_maximize_restore_text),
-                null
-            )
+        // Update a11y announcement to say "double tap to maximize or restore window size"
+        ViewCompat.replaceAccessibilityAction(
+            maximizeWindowButton,
+            AccessibilityActionCompat.ACTION_CLICK,
+            context.getString(R.string.maximize_button_talkback_action_maximize_restore_text),
+            null
+        )
 
-            // Update a11y read out to say "double tap to minimize app window"
-            ViewCompat.replaceAccessibilityAction(
-                minimizeWindowButton,
-                AccessibilityActionCompat.ACTION_CLICK,
-                getString(R.string.minimize_button_talkback_action_maximize_restore_text),
-                null
-            )
-        }
+        // Update a11y announcement out to say "double tap to minimize app window"
+        ViewCompat.replaceAccessibilityAction(
+            minimizeWindowButton,
+            AccessibilityActionCompat.ACTION_CLICK,
+            context.getString(R.string.minimize_button_talkback_action_maximize_restore_text),
+            null
+        )
     }
 
     override fun bindData(data: HeaderData) {
@@ -275,7 +266,8 @@
     /** Sets the app's name in the header. */
     fun setAppName(name: CharSequence) {
         appNameTextView.text = name
-        openMenuButton.contentDescription = name
+        openMenuButton.contentDescription =
+            context.getString(R.string.desktop_mode_app_header_chip_text, name)
     }
 
     /** Sets the app's icon in the header. */
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 19829e7..bac8e50 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -12,7 +12,6 @@
 jorgegil@google.com
 vaniadesmonda@google.com
 pbdr@google.com
-tkachenkoi@google.com
 mpodolian@google.com
 jeremysim@google.com
 peanutbutter@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index 02b2cec..ae73dae 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -53,10 +53,12 @@
         <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
         <option name="run-command" value="settings put system show_touches 1"/>
         <option name="run-command" value="settings put system pointer_location 1"/>
+        <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
         <option name="teardown-command"
                 value="settings delete secure show_ime_with_hard_keyboard"/>
         <option name="teardown-command" value="settings delete system show_touches"/>
         <option name="teardown-command" value="settings delete system pointer_location"/>
+        <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
         <option name="teardown-command"
                 value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
     </target_preparer>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index ffcc344..7a7d88b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -572,6 +572,22 @@
         assertThat(update.shouldShowEducation).isTrue();
     }
 
+    /** Verifies that the update should contain the bubble bar location. */
+    @Test
+    public void test_shouldUpdateBubbleBarLocation() {
+        // Setup
+        mBubbleData.setListener(mListener);
+
+        // Test
+        mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */
+                true, BubbleBarLocation.LEFT);
+
+        // Verify
+        verifyUpdateReceived();
+        BubbleData.Update update = mUpdateCaptor.getValue();
+        assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT);
+    }
+
     /**
      * Verifies that the update shouldn't show the user education, if the education is required but
      * the bubble should auto-expand
@@ -1367,6 +1383,20 @@
     }
 
     @Test
+    public void setSelectedBubbleAndExpandStackWithLocation() {
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        sendUpdatedEntryAtTime(mEntryA2, 2000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT);
+
+        verifyUpdateReceived();
+        assertSelectionChangedTo(mBubbleA1);
+        assertExpandedChangedTo(true);
+        assertLocationChangedTo(BubbleBarLocation.LEFT);
+    }
+
+    @Test
     public void testShowOverflowChanged_hasOverflowBubbles() {
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
         sendUpdatedEntryAtTime(mEntryA1, 1000);
@@ -1450,6 +1480,12 @@
         assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
     }
 
+    private void assertLocationChangedTo(BubbleBarLocation location) {
+        BubbleData.Update update = mUpdateCaptor.getValue();
+        assertWithMessage("locationChanged").that(update.mBubbleBarLocation)
+                .isEqualTo(location);
+    }
+
     private void assertExpandedChangedTo(boolean expected) {
         BubbleData.Update update = mUpdateCaptor.getValue();
         assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index d6b13610..70a30a3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -113,7 +113,7 @@
                 .strictness(Strictness.LENIENT)
                 .spyStatic(DesktopModeStatus::class.java)
                 .startMocking()
-        doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
 
         testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
         shellInit = spy(ShellInit(testExecutor))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index 403d468..d510570 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -30,7 +30,6 @@
 import android.window.DisplayAreaInfo
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
@@ -48,7 +47,6 @@
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
@@ -107,12 +105,7 @@
     @Before
     fun setUp() {
         Dispatchers.setMain(StandardTestDispatcher())
-        mockitoSession =
-            mockitoSession()
-                .strictness(Strictness.LENIENT)
-                .spyStatic(DesktopModeStatus::class.java)
-                .startMocking()
-        doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+        mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking()
 
         testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
         shellInit = spy(ShellInit(testExecutor))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index edb9b2d..be53272 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -51,6 +51,7 @@
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableContext
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
 import android.view.Gravity
@@ -147,6 +148,7 @@
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
 import com.google.common.truth.Truth.assertThat
@@ -171,7 +173,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.isA
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mock
@@ -187,7 +188,6 @@
 import org.mockito.kotlin.argThat
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.atLeastOnce
-import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
@@ -257,6 +257,7 @@
     @Mock private lateinit var desksOrganizer: DesksOrganizer
     @Mock private lateinit var userProfileContexts: UserProfileContexts
     @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
+    @Mock private lateinit var packageManager: PackageManager
 
     private lateinit var controller: DesktopTasksController
     private lateinit var shellInit: ShellInit
@@ -266,6 +267,7 @@
     private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
     private lateinit var testScope: CoroutineScope
     private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
+    private lateinit var spyContext: TestableContext
 
     private val shellExecutor = TestShellExecutor()
 
@@ -282,6 +284,7 @@
     private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
     private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
     private val wallpaperToken = MockToken().token()
+    private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
 
     @Before
     fun setUp() {
@@ -292,9 +295,10 @@
                 .spyStatic(DesktopModeStatus::class.java)
                 .spyStatic(Toast::class.java)
                 .startMocking()
-        doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
 
         testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+        spyContext = spy(mContext)
         shellInit = spy(ShellInit(testExecutor))
         userRepositories =
             DesktopUserRepositories(
@@ -316,7 +320,7 @@
                 mContext,
                 mockHandler,
             )
-        desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(context))
+        desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext))
 
         whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
         whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
@@ -363,9 +367,9 @@
 
         shellInit.init()
 
-        val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
+        val captor = argumentCaptor<RecentsTransitionStateListener>()
         verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
-        recentsTransitionStateListener = captor.value
+        recentsTransitionStateListener = captor.firstValue
 
         controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
 
@@ -374,6 +378,9 @@
         taskRepository = userRepositories.current
         taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY)
         taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY)
+
+        spyContext.setMockPackageManager(packageManager)
+        whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(homeComponentName)
     }
 
     private fun createController() =
@@ -441,7 +448,7 @@
     fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() {
         val task1 = setUpFreeformTask()
 
-        val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        val argumentCaptor = argumentCaptor<Boolean>()
         controller.toggleDesktopTaskSize(
             task1,
             ToggleTaskSizeInteraction(
@@ -461,7 +468,7 @@
                 STABLE_BOUNDS.height(),
                 displayController,
             )
-        assertThat(argumentCaptor.value).isTrue()
+        assertThat(argumentCaptor.firstValue).isTrue()
     }
 
     @Test
@@ -476,7 +483,7 @@
         val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
         val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
 
-        val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        val argumentCaptor = argumentCaptor<Boolean>()
         controller.toggleDesktopTaskSize(
             task1,
             ToggleTaskSizeInteraction(
@@ -497,7 +504,7 @@
                 eq(displayController),
                 anyOrNull(),
             )
-        assertThat(argumentCaptor.value).isFalse()
+        assertThat(argumentCaptor.firstValue).isFalse()
     }
 
     @Test
@@ -547,6 +554,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
         markTaskHidden(task1)
@@ -581,7 +589,7 @@
         val wct =
             getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
         // Wallpaper is moved to front.
-        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+        wct.assertReorderAt(index = 0, wallpaperToken)
         // Desk is activated.
         verify(desksOrganizer).activateDesk(wct, deskId)
     }
@@ -783,6 +791,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
         markTaskVisible(task1)
@@ -825,7 +834,8 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
-    fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
+    fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
         markTaskHidden(task1)
@@ -842,6 +852,24 @@
         wct.assertReorderAt(index = 2, task2)
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskVisible(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: wallpaper intent, task1, task2
+        wct.assertReorderAt(index = 0, wallpaperToken)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
     @Test
     @DisableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
@@ -860,9 +888,9 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-    fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
-        whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
-            .thenReturn(Binder())
+    fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+
         controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
         val wct =
@@ -871,10 +899,18 @@
     }
 
     @Test
-    @DisableFlags(
-        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
-    )
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() {
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        wct.assertReorderAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
         taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
         val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
@@ -899,6 +935,7 @@
     fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
         whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
             .thenReturn(Binder())
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
         val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
         val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
@@ -991,6 +1028,7 @@
     /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val homeTask = setUpHomeTask()
         val freeformTask = setUpFreeformTask()
         val minimizedTask = setUpFreeformTask()
@@ -1569,6 +1607,7 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val task = createTaskInfo(1)
         whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
         whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
@@ -1736,7 +1775,7 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() {
-        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
         whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
             .thenReturn(Binder())
 
@@ -1751,12 +1790,12 @@
 
         verify(desktopModeEnterExitTransitionListener)
             .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
     }
 
     @Test
     fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() {
-        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
         whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
             .thenReturn(Binder())
 
@@ -1768,7 +1807,7 @@
 
         verify(desktopModeEnterExitTransitionListener)
             .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
     }
 
     @Test
@@ -1802,6 +1841,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val freeformTask = setUpFreeformTask()
         val fullscreenTask = setUpFullscreenTask()
         markTaskHidden(freeformTask)
@@ -1828,6 +1868,7 @@
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
         Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() {
         val freeformTask = setUpFreeformTask()
@@ -1840,7 +1881,7 @@
         )
 
         val wct = getLatestEnterDesktopWct()
-        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+        wct.assertReorderAt(index = 0, wallpaperToken)
         verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask)
         verify(desksOrganizer).activateDesk(wct, deskId = 0)
         verify(desktopModeEnterExitTransitionListener)
@@ -1967,6 +2008,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
         val newTask = setUpFullscreenTask()
         val homeTask = setUpHomeTask()
@@ -2224,26 +2266,26 @@
     fun moveTaskToFront_remoteTransition_usesOneshotHandler() {
         setUpHomeTask()
         val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() }
-        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
         whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
             .thenReturn(Binder())
 
         controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
 
-        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
     }
 
     @Test
     fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() {
         setUpHomeTask()
         val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() }
-        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
         whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
             .thenReturn(Binder())
 
         controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
 
-        assertThat(transitionHandlerArgCaptor.value)
+        assertThat(transitionHandlerArgCaptor.firstValue)
             .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java)
     }
 
@@ -2718,9 +2760,9 @@
 
         controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-        captor.value.hierarchyOps.none { hop ->
+        captor.firstValue.hierarchyOps.none { hop ->
             hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
         }
     }
@@ -2759,9 +2801,9 @@
 
         controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startPipTransition(captor.capture())
-        captor.value.hierarchyOps.none { hop ->
+        captor.firstValue.hierarchyOps.none { hop ->
             hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
         }
     }
@@ -2775,9 +2817,9 @@
 
         controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-        captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
+        captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
     }
 
     @Test
@@ -2791,10 +2833,10 @@
         // The only active task is being minimized.
         controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
         // Adds remove wallpaper operation
-        captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+        captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
     }
 
     @Test
@@ -2808,9 +2850,9 @@
         // The only active task is already minimized.
         controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-        captor.value.hierarchyOps.none { hop ->
+        captor.firstValue.hierarchyOps.none { hop ->
             hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
         }
     }
@@ -2825,9 +2867,9 @@
 
         controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
 
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-        captor.value.hierarchyOps.none { hop ->
+        captor.firstValue.hierarchyOps.none { hop ->
             hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
         }
     }
@@ -2845,10 +2887,10 @@
         // task1 is the only visible task as task2 is minimized.
         controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
         // Adds remove wallpaper operation
-        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val captor = argumentCaptor<WindowContainerTransaction>()
         verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
         // Adds remove wallpaper operation
-        captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+        captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
     }
 
     @Test
@@ -2987,6 +3029,7 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
         val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
         tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -3118,6 +3161,7 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val freeformTask1 = setUpFreeformTask()
         val freeformTask2 = createFreeformTask()
 
@@ -3152,7 +3196,9 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val task = createFreeformTask()
+
         val result = controller.handleRequest(Binder(), createTransition(task))
 
         assertNotNull(result, "Should handle request")
@@ -3180,6 +3226,7 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
         val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
         // Second display task
         createFreeformTask(displayId = SECOND_DISPLAY)
@@ -4635,7 +4682,7 @@
 
         controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
 
-        val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val wctArgument = argumentCaptor<WindowContainerTransaction>()
         verify(splitScreenController)
             .requestEnterSplitSelect(
                 eq(task2),
@@ -4643,9 +4690,9 @@
                 eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
                 eq(task2.configuration.windowConfiguration.bounds),
             )
-        assertThat(wctArgument.value.hierarchyOps).hasSize(1)
+        assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1)
         // Removes wallpaper activity when leaving desktop
-        wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+        wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
     }
 
     @Test
@@ -4660,7 +4707,7 @@
 
         controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
 
-        val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val wctArgument = argumentCaptor<WindowContainerTransaction>()
         verify(splitScreenController)
             .requestEnterSplitSelect(
                 eq(task2),
@@ -4669,7 +4716,7 @@
                 eq(task2.configuration.windowConfiguration.bounds),
             )
         // Does not remove wallpaper activity, as desktop still has visible desktop tasks
-        assertThat(wctArgument.value.hierarchyOps).isEmpty()
+        assertThat(wctArgument.firstValue.hierarchyOps).isEmpty()
     }
 
     @Test
@@ -4677,7 +4724,7 @@
     fun newWindow_fromFullscreenOpensInSplit() {
         setUpLandscapeDisplay()
         val task = setUpFullscreenTask()
-        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        val optionsCaptor = argumentCaptor<Bundle>()
         runOpenNewWindow(task)
         verify(splitScreenController)
             .startIntent(
@@ -4690,7 +4737,7 @@
                 eq(true),
                 eq(SPLIT_INDEX_UNDEFINED),
             )
-        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
             .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
     }
 
@@ -4699,7 +4746,7 @@
     fun newWindow_fromSplitOpensInSplit() {
         setUpLandscapeDisplay()
         val task = setUpSplitScreenTask()
-        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        val optionsCaptor = argumentCaptor<Bundle>()
         runOpenNewWindow(task)
         verify(splitScreenController)
             .startIntent(
@@ -4712,7 +4759,7 @@
                 eq(true),
                 eq(SPLIT_INDEX_UNDEFINED),
             )
-        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
             .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
     }
 
@@ -4807,11 +4854,11 @@
         setUpLandscapeDisplay()
         val task = setUpFullscreenTask()
         val taskToRequest = setUpFreeformTask()
-        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        val optionsCaptor = argumentCaptor<Bundle>()
         runOpenInstance(task, taskToRequest.taskId)
         verify(splitScreenController)
             .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
-        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
             .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
     }
 
@@ -4821,11 +4868,11 @@
         setUpLandscapeDisplay()
         val task = setUpSplitScreenTask()
         val taskToRequest = setUpFreeformTask()
-        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        val optionsCaptor = argumentCaptor<Bundle>()
         runOpenInstance(task, taskToRequest.taskId)
         verify(splitScreenController)
             .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
-        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
             .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
     }
 
@@ -5427,38 +5474,90 @@
     }
 
     @Test
-    fun onUnhandledDrag_newFreeformIntent() {
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagEnabled() {
         testOnUnhandledDrag(
             DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
             PointF(1200f, 700f),
             Rect(240, 700, 2160, 1900),
+            tabTearingAnimationFlagEnabled = true,
         )
     }
 
     @Test
-    fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagDisabled() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+            PointF(1200f, 700f),
+            Rect(240, 700, 2160, 1900),
+            tabTearingAnimationFlagEnabled = false,
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagEnabled() {
         testOnUnhandledDrag(
             DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
             PointF(50f, 700f),
             Rect(0, 0, 500, 1000),
+            tabTearingAnimationFlagEnabled = true,
         )
     }
 
     @Test
-    fun onUnhandledDrag_newFreeformIntentSplitRight() {
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagDisabled() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+            PointF(50f, 700f),
+            Rect(0, 0, 500, 1000),
+            tabTearingAnimationFlagEnabled = false,
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagEnabled() {
         testOnUnhandledDrag(
             DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
             PointF(2500f, 700f),
             Rect(500, 0, 1000, 1000),
+            tabTearingAnimationFlagEnabled = true,
         )
     }
 
     @Test
-    fun onUnhandledDrag_newFullscreenIntent() {
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagDisabled() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+            PointF(2500f, 700f),
+            Rect(500, 0, 1000, 1000),
+            tabTearingAnimationFlagEnabled = false,
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagEnabled() {
         testOnUnhandledDrag(
             DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
             PointF(1200f, 50f),
             Rect(),
+            tabTearingAnimationFlagEnabled = true,
+        )
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+    fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagDisabled() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+            PointF(1200f, 50f),
+            Rect(),
+            tabTearingAnimationFlagEnabled = false,
         )
     }
 
@@ -5812,6 +5911,7 @@
         indicatorType: DesktopModeVisualIndicator.IndicatorType,
         inputCoordinate: PointF,
         expectedBounds: Rect,
+        tabTearingAnimationFlagEnabled: Boolean,
     ) {
         setUpLandscapeDisplay()
         val task = setUpFreeformTask()
@@ -5842,6 +5942,16 @@
                 anyOrNull(),
                 eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT),
             )
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    eq(TRANSIT_OPEN),
+                    any(),
+                    anyOrNull(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(Binder())
 
         spyController.onUnhandledDrag(
             mockPendingIntent,
@@ -5849,24 +5959,37 @@
             mockDragEvent,
             mockCallback as Consumer<Boolean>,
         )
-        val arg: ArgumentCaptor<WindowContainerTransaction> =
-            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         var expectedWindowingMode: Int
         if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
             expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
             // Fullscreen launches currently use default transitions
-            verify(transitions).startTransition(any(), capture(arg), anyOrNull())
+            verify(transitions).startTransition(any(), arg.capture(), anyOrNull())
         } else {
             expectedWindowingMode = WINDOWING_MODE_FREEFORM
-            // All other launches use a special handler.
-            verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+            if (tabTearingAnimationFlagEnabled) {
+                verify(desktopMixedTransitionHandler)
+                    .startLaunchTransition(
+                        eq(TRANSIT_OPEN),
+                        arg.capture(),
+                        anyOrNull(),
+                        anyOrNull(),
+                        anyOrNull(),
+                    )
+            } else {
+                // All other launches use a special handler.
+                verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture())
+            }
         }
         assertThat(
-                ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+                ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions)
                     .launchWindowingMode
             )
             .isEqualTo(expectedWindowingMode)
-        assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds)
+        assertThat(
+                ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions)
+                    .launchBounds
+            )
             .isEqualTo(expectedBounds)
     }
 
@@ -6048,52 +6171,49 @@
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
         handlerClass: Class<out TransitionHandler>? = null,
     ): WindowContainerTransaction {
-        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         if (handlerClass == null) {
             verify(transitions).startTransition(eq(type), arg.capture(), isNull())
         } else {
             verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
         }
-        return arg.value
+        return arg.lastValue
     }
 
     private fun getLatestToggleResizeDesktopTaskWct(
         currentBounds: Rect? = null
     ): WindowContainerTransaction {
-        val arg: ArgumentCaptor<WindowContainerTransaction> =
-            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
-            .startTransition(capture(arg), eq(currentBounds))
-        return arg.value
+            .startTransition(arg.capture(), eq(currentBounds))
+        return arg.lastValue
     }
 
     private fun getLatestDesktopMixedTaskWct(
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN
     ): WindowContainerTransaction {
-        val arg: ArgumentCaptor<WindowContainerTransaction> =
-            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         verify(desktopMixedTransitionHandler)
-            .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull())
-        return arg.value
+            .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull())
+        return arg.lastValue
     }
 
     private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
-        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
-        return arg.value
+        return arg.lastValue
     }
 
     private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
-        val arg: ArgumentCaptor<WindowContainerTransaction> =
-            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-        verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
-        return arg.value
+        val arg = argumentCaptor<WindowContainerTransaction>()
+        verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture())
+        return arg.lastValue
     }
 
     private fun getLatestExitDesktopWct(): WindowContainerTransaction {
-        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        val arg = argumentCaptor<WindowContainerTransaction>()
         verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
-        return arg.value
+        return arg.lastValue
     }
 
     private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
index 83e4872..030bb1ac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
@@ -123,8 +123,26 @@
         assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2)
     }
 
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
+    fun getUserForProfile_flagEnabled_returnsUserIdForProfile() {
+        userRepositories.onUserChanged(USER_ID_2, mock())
+        val profiles: MutableList<UserInfo> =
+            mutableListOf(
+                UserInfo(USER_ID_2, "User profile", 0),
+                UserInfo(PROFILE_ID_1, "Work profile", 0),
+            )
+        userRepositories.onUserProfilesChanged(profiles)
+
+        val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1)
+
+        assertThat(userIdForProfile).isEqualTo(USER_ID_2)
+    }
+
     private companion object {
         const val USER_ID_1 = 7
+        const val USER_ID_2 = 8
+        const val PROFILE_ID_1 = 4
         const val PROFILE_ID_2 = 5
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 25246d9..1732875 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -70,6 +70,7 @@
     @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var draggedTaskLeash: SurfaceControl
     @Mock private lateinit var homeTaskLeash: SurfaceControl
+    @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories
 
     private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
 
@@ -84,6 +85,7 @@
                     context,
                     transitions,
                     taskDisplayAreaOrganizer,
+                    desktopUserRepositories,
                     mockInteractionJankMonitor,
                     transactionSupplier,
                 )
@@ -93,6 +95,7 @@
                     context,
                     transitions,
                     taskDisplayAreaOrganizer,
+                    desktopUserRepositories,
                     mockInteractionJankMonitor,
                     transactionSupplier,
                 )
@@ -484,17 +487,22 @@
         val mergedFinishTransaction = mock<SurfaceControl.Transaction>()
         val finishCallback = mock<Transitions.TransitionFinishCallback>()
         val task = createTask()
-        val startTransition = startDrag(
-            springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null)
+        val startTransition =
+            startDrag(
+                springHandler,
+                task,
+                finishTransaction = playingFinishTransaction,
+                homeChange = null,
+            )
         springHandler.onTaskResizeAnimationListener = mock()
 
         springHandler.mergeAnimation(
             transition = mock<IBinder>(),
             info =
-            createTransitionInfo(
-                type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
-                draggedTask = task,
-            ),
+                createTransitionInfo(
+                    type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+                    draggedTask = task,
+                ),
             startT = mergedStartTransaction,
             finishT = mergedFinishTransaction,
             mergeTarget = startTransition,
@@ -723,7 +731,8 @@
     private fun createTransitionInfo(
         type: Int,
         draggedTask: RunningTaskInfo,
-        homeChange: TransitionInfo.Change? = createHomeChange()) =
+        homeChange: TransitionInfo.Change? = createHomeChange(),
+    ) =
         TransitionInfo(type, /* flags= */ 0).apply {
             homeChange?.let { addChange(it) }
             addChange( // Dragged Task.
@@ -741,11 +750,12 @@
             )
         }
 
-    private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply {
-        parent = null
-        taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
-        flags = flags or FLAG_IS_WALLPAPER
-    }
+    private fun createHomeChange() =
+        TransitionInfo.Change(mock(), homeTaskLeash).apply {
+            parent = null
+            taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+            flags = flags or FLAG_IS_WALLPAPER
+        }
 
     private fun systemPropertiesKey(name: String) =
         "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
index dfb1b0c..9cb2a05 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
@@ -16,9 +16,12 @@
 
 package com.android.wm.shell.desktopmode.compatui
 
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Binder
 import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_CLOSE
@@ -37,6 +40,7 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -44,6 +48,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -62,16 +67,23 @@
     private val desktopRepository = mock<DesktopRepository>()
     private val startT = mock<SurfaceControl.Transaction>()
     private val finishT = mock<SurfaceControl.Transaction>()
+    private val packageManager = mock<PackageManager>()
+    private val componentName = mock<ComponentName>()
 
+    private lateinit var spyContext: TestableContext
     private lateinit var transitionHandler: SystemModalsTransitionHandler
     private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
 
     @Before
     fun setUp() {
+        spyContext = spy(mContext)
         // Simulate having one Desktop task so that we see Desktop Mode as active
         whenever(desktopUserRepositories.current).thenReturn(desktopRepository)
         whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1)
-        desktopModeCompatPolicy = DesktopModeCompatPolicy(context)
+        whenever(spyContext.packageManager).thenReturn(packageManager)
+        whenever(componentName.packageName).thenReturn(HOME_LAUNCHER_PACKAGE_NAME)
+        whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(componentName)
+        desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext)
         transitionHandler = createTransitionHandler()
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 86e8142..08b9e04 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -26,6 +26,8 @@
 import com.android.window.flags.Flags
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.desktopmode.CaptionState
+import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger
+import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_DELAY_MILLIS
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.TOOLTIP_VISIBLE_DURATION_MILLIS
@@ -86,6 +88,7 @@
     @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository
     @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
     @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController
+    @Mock private lateinit var mockDesktopModeUiEventLogger: DesktopModeUiEventLogger
 
     @Before
     fun setUp() {
@@ -105,6 +108,7 @@
                 mockTooltipController,
                 testScope.backgroundScope,
                 Dispatchers.Main,
+                mockDesktopModeUiEventLogger,
             )
     }
 
@@ -123,6 +127,8 @@
             verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
             verify(mockDataStoreRepository, times(1))
                 .updateAppHandleHintViewedTimestampMillis(eq(true))
+            verify(mockDesktopModeUiEventLogger, times(1))
+                .log(any(), eq(DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_SHOWN))
         }
 
     @Test
@@ -155,6 +161,8 @@
             verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
             verify(mockDataStoreRepository, times(1))
                 .updateEnterDesktopModeHintViewedTimestampMillis(eq(true))
+            verify(mockDesktopModeUiEventLogger, times(1))
+                .log(any(), eq(DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN))
         }
 
     @Test
@@ -170,6 +178,8 @@
             verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
             verify(mockDataStoreRepository, times(1))
                 .updateExitDesktopModeHintViewedTimestampMillis(eq(true))
+            verify(mockDesktopModeUiEventLogger, times(1))
+                .log(any(), eq(DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN))
         }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index b50af74..439be91 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -17,9 +17,13 @@
 package com.android.wm.shell.recents;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX;
 import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING;
 import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING;
 import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED;
@@ -44,9 +48,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
 
@@ -57,6 +63,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.internal.os.IResultReceiver;
+import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
@@ -92,9 +99,13 @@
 @SmallTest
 public class RecentsTransitionHandlerTest extends ShellTestCase {
 
+    private static final int FREEFORM_TASK_CORNER_RADIUS = 32;
+
     @Mock
     private Context mContext;
     @Mock
+    private Resources mResources;
+    @Mock
     private TaskStackListenerImpl mTaskStackListener;
     @Mock
     private ShellCommandHandler mShellCommandHandler;
@@ -134,6 +145,10 @@
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
         when(mContext.getSystemService(KeyguardManager.class))
                 .thenReturn(mock(KeyguardManager.class));
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getDimensionPixelSize(
+                R.dimen.desktop_windowing_freeform_rounded_corner_radius)
+        ).thenReturn(FREEFORM_TASK_CORNER_RADIUS);
         mShellInit = spy(new ShellInit(mMainExecutor));
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
                 mDisplayInsetsController, mMainExecutor));
@@ -276,6 +291,57 @@
         assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
     }
 
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() {
+        ActivityManager.RunningTaskInfo freeformTask =
+                new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, freeformTask)
+                .build();
+        SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash();
+        final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+        SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+        mRecentsTransitionHandler.startAnimation(
+                transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+                mock(Transitions.TransitionFinishCallback.class));
+
+        mRecentsTransitionHandler.findController(transition).merge(
+                mergeTransitionInfo,
+                new StubTransaction(),
+                finishT,
+                transition,
+                mock(Transitions.TransitionFinishCallback.class));
+        mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+                false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+        mMainExecutor.flushAll();
+
+        verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS);
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    public void testFinish_returningToFreeformTasks_setsCornerRadius() {
+        ActivityManager.RunningTaskInfo freeformTask =
+                new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_CLOSE, freeformTask)
+                .build();
+        SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash();
+        final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+        SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+        mRecentsTransitionHandler.startAnimation(
+                transition, transitionInfo, new StubTransaction(), finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+                false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+        mMainExecutor.flushAll();
+
+
+        verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS);
+    }
+
     private IBinder startRecentsTransition(boolean synthetic) {
         return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class));
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
index e28d6ff..fd22a84 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.wm.shell.shared.bubbles
 
+import android.content.Context
 import android.graphics.Insets
 import android.graphics.Rect
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker
@@ -27,11 +29,14 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit
+
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 /** Unit tests for [DragZoneFactory]. */
 class DragZoneFactoryTest {
 
+    private val context = getApplicationContext<Context>()
     private lateinit var dragZoneFactory: DragZoneFactory
     private val tabletPortrait =
         DeviceConfig(
@@ -55,184 +60,238 @@
     @Test
     fun dragZonesForBubbleBar_tablet() {
         dragZoneFactory =
-            DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+            DragZoneFactory(
+                context,
+                tabletPortrait,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.Bubble::class.java,
-                DragZone.Bubble::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForBubble_tablet_portrait() {
         dragZoneFactory =
-            DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+            DragZoneFactory(
+                context,
+                tabletPortrait,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.DesktopWindow::class.java,
-                DragZone.Split.Top::class.java,
-                DragZone.Split.Bottom::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.DesktopWindow>(),
+                verifyInstance<DragZone.Split.Top>(),
+                verifyInstance<DragZone.Split.Bottom>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForBubble_tablet_landscape() {
-        dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                tabletLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.DesktopWindow::class.java,
-                DragZone.Split.Left::class.java,
-                DragZone.Split.Right::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.DesktopWindow>(),
+                verifyInstance<DragZone.Split.Left>(),
+                verifyInstance<DragZone.Split.Right>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForBubble_foldable_portrait() {
-        dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldablePortrait,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.Split.Left::class.java,
-                DragZone.Split.Right::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.Split.Left>(),
+                verifyInstance<DragZone.Split.Right>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForBubble_foldable_landscape() {
-        dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldableLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.Split.Top::class.java,
-                DragZone.Split.Bottom::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.Split.Top>(),
+                verifyInstance<DragZone.Split.Bottom>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForExpandedView_tablet_portrait() {
         dragZoneFactory =
-            DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+            DragZoneFactory(
+                context,
+                tabletPortrait,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(
                 DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
             )
-        val expectedZones: List<Class<out DragZone>> =
+        val expectedZones: List<DragZoneVerifier> =
             listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.DesktopWindow::class.java,
-                DragZone.Split.Top::class.java,
-                DragZone.Split.Bottom::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.DesktopWindow>(),
+                verifyInstance<DragZone.Split.Top>(),
+                verifyInstance<DragZone.Split.Bottom>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForExpandedView_tablet_landscape() {
-        dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
-        val dragZones =
-            dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
-            listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.DesktopWindow::class.java,
-                DragZone.Split.Left::class.java,
-                DragZone.Split.Right::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                tabletLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        val dragZones =
+            dragZoneFactory.createSortedDragZones(
+                DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+            )
+        val expectedZones: List<DragZoneVerifier> =
+            listOf(
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.DesktopWindow>(),
+                verifyInstance<DragZone.Split.Left>(),
+                verifyInstance<DragZone.Split.Right>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+            )
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForExpandedView_foldable_portrait() {
-        dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
-        val dragZones =
-            dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
-            listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.Split.Left::class.java,
-                DragZone.Split.Right::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldablePortrait,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        val dragZones =
+            dragZoneFactory.createSortedDragZones(
+                DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+            )
+        val expectedZones: List<DragZoneVerifier> =
+            listOf(
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.Split.Left>(),
+                verifyInstance<DragZone.Split.Right>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+            )
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForExpandedView_foldable_landscape() {
-        dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
-        val dragZones =
-            dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
-        val expectedZones: List<Class<out DragZone>> =
-            listOf(
-                DragZone.Dismiss::class.java,
-                DragZone.FullScreen::class.java,
-                DragZone.Split.Top::class.java,
-                DragZone.Split.Bottom::class.java,
-                DragZone.Bubble.Left::class.java,
-                DragZone.Bubble.Right::class.java,
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldableLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
             )
-        dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
-            assertThat(zone).isInstanceOf(expectedType)
-        }
+        val dragZones =
+            dragZoneFactory.createSortedDragZones(
+                DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+            )
+        val expectedZones: List<DragZoneVerifier> =
+            listOf(
+                verifyInstance<DragZone.Dismiss>(),
+                verifyInstance<DragZone.FullScreen>(),
+                verifyInstance<DragZone.Split.Top>(),
+                verifyInstance<DragZone.Split.Bottom>(),
+                verifyInstance<DragZone.Bubble.Left>(),
+                verifyInstance<DragZone.Bubble.Right>(),
+            )
+        assertThat(dragZones).hasSize(expectedZones.size)
+        dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
     }
 
     @Test
     fun dragZonesForBubble_tablet_desktopModeDisabled() {
         isDesktopWindowModeSupported = false
-        dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldableLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
             dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
         assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
@@ -241,9 +300,21 @@
     @Test
     fun dragZonesForExpandedView_tablet_desktopModeDisabled() {
         isDesktopWindowModeSupported = false
-        dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+        dragZoneFactory =
+            DragZoneFactory(
+                context,
+                foldableLandscape,
+                splitScreenModeChecker,
+                desktopWindowModeChecker
+            )
         val dragZones =
-            dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+            dragZoneFactory.createSortedDragZones(
+                DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+            )
         assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
     }
+
+    private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone ->
+        assertThat(dragZone).isInstanceOf(T::class.java)
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
new file mode 100644
index 0000000..efb91c5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFails
+
+/** Unit tests for [DropTargetManager]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DropTargetManagerTest {
+
+    private lateinit var dropTargetManager: DropTargetManager
+    private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener
+    private val dropTarget = Rect(0, 0, 0, 0)
+
+    // create 3 drop zones that are horizontally next to each other
+    // -------------------------------------------------
+    // |               |               |               |
+    // |    bubble     |               |    bubble     |
+    // |               |    dismiss    |               |
+    // |     left      |               |     right     |
+    // |               |               |               |
+    // -------------------------------------------------
+    private val bubbleLeftDragZone =
+        DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget)
+    private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100))
+    private val bubbleRightDragZone =
+        DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget)
+
+    @Before
+    fun setUp() {
+        dragZoneChangedListener = FakeDragZoneChangedListener()
+        dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener)
+    }
+
+    @Test
+    fun onDragStarted_notifiesInitialDragZone() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone)
+        )
+        assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone)
+    }
+
+    @Test
+    fun onDragStarted_missingExpectedDragZone_fails() {
+        assertFails {
+            dropTargetManager.onDragStarted(
+                DraggedObject.Bubble(BubbleBarLocation.RIGHT),
+                listOf(bubbleLeftDragZone)
+            )
+        }
+    }
+
+    @Test
+    fun onDragUpdated_notifiesDragZoneChanged() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+        )
+        dropTargetManager.onDragUpdated(
+            bubbleRightDragZone.bounds.centerX(),
+            bubbleRightDragZone.bounds.centerY()
+        )
+        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+        dropTargetManager.onDragUpdated(
+            dismissDragZone.bounds.centerX(),
+            dismissDragZone.bounds.centerY()
+        )
+        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+    }
+
+    @Test
+    fun onDragUpdated_withinSameZone_doesNotNotify() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+        )
+        dropTargetManager.onDragUpdated(
+            bubbleLeftDragZone.bounds.centerX(),
+            bubbleLeftDragZone.bounds.centerY()
+        )
+        assertThat(dragZoneChangedListener.fromDragZone).isNull()
+        assertThat(dragZoneChangedListener.toDragZone).isNull()
+    }
+
+    @Test
+    fun onDragUpdated_outsideAllZones_doesNotNotify() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone)
+        )
+        val pointX = 200
+        val pointY = 200
+        assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse()
+        assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse()
+        dropTargetManager.onDragUpdated(pointX, pointY)
+        assertThat(dragZoneChangedListener.fromDragZone).isNull()
+        assertThat(dragZoneChangedListener.toDragZone).isNull()
+    }
+
+    @Test
+    fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() {
+        // create a drag zone that spans across the width of all 3 drag zones, but extends below
+        // them
+        val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200))
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone)
+        )
+
+        // drag to a point that is within both the bubble right zone and split zone
+        val (pointX, pointY) =
+            Pair(
+                bubbleRightDragZone.bounds.centerX(),
+                bubbleRightDragZone.bounds.centerY()
+            )
+        assertThat(splitDragZone.contains(pointX, pointY)).isTrue()
+        dropTargetManager.onDragUpdated(pointX, pointY)
+        // verify we dragged to the bubble right zone because that has higher priority than split
+        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+        dropTargetManager.onDragUpdated(
+            bubbleRightDragZone.bounds.centerX(),
+            150 // below the bubble and dismiss drag zones but within split
+        )
+        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone)
+
+        val (dismissPointX, dismissPointY) =
+            Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY())
+        assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue()
+        dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
+        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone)
+        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+    }
+
+    @Test
+    fun onDragUpdated_afterDragEnded_doesNotNotify() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+        )
+        dropTargetManager.onDragEnded()
+        dropTargetManager.onDragUpdated(
+            bubbleRightDragZone.bounds.centerX(),
+            bubbleRightDragZone.bounds.centerY()
+        )
+        assertThat(dragZoneChangedListener.fromDragZone).isNull()
+        assertThat(dragZoneChangedListener.toDragZone).isNull()
+    }
+
+    private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {
+        var initialDragZone: DragZone? = null
+        var fromDragZone: DragZone? = null
+        var toDragZone: DragZone? = null
+
+        override fun onInitialDragZoneSet(dragZone: DragZone) {
+            initialDragZone = dragZone
+        }
+        override fun onDragZoneChanged(from: DragZone, to: DragZone) {
+            fromDragZone = from
+            toDragZone = to
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
index 55e9de5..f69bf34 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
@@ -30,6 +30,7 @@
 import com.android.window.flags.Flags
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
 import org.junit.Assert.assertFalse
@@ -52,10 +53,14 @@
 class DesktopModeCompatPolicyTest : ShellTestCase() {
     @get:Rule val compatRule = PlatformCompatChangeRule()
     private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
+    private val packageManager: PackageManager = mock()
+    private val homeActivities = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
 
     @Before
     fun setUp() {
         desktopModeCompatPolicy = DesktopModeCompatPolicy(mContext)
+        whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
+        mContext.setMockPackageManager(packageManager)
     }
 
     @Test
@@ -128,10 +133,6 @@
 
     @Test
     fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage() {
-        val packageManager: PackageManager = mock()
-        val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
-        whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
-        mContext.setMockPackageManager(packageManager)
         assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
             createFreeformTask(/* displayId */ 0)
                 .apply {
@@ -142,10 +143,6 @@
 
     @Test
     fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage_notDisplayed() {
-        val packageManager: PackageManager = mock()
-        val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
-        whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
-        mContext.setMockPackageManager(packageManager)
         assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
             createFreeformTask(/* displayId */ 0)
                 .apply {
@@ -155,6 +152,21 @@
     }
 
     @Test
+    fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage_notYetAvailable() {
+        val emptyHomeActivities: ComponentName = mock()
+        mContext.setMockPackageManager(packageManager)
+
+        whenever(emptyHomeActivities.packageName).thenReturn(null)
+        whenever(packageManager.getHomeActivities(any())).thenReturn(emptyHomeActivities)
+
+        assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
+            createFreeformTask(/* displayId */ 0)
+                .apply {
+                    isTopActivityNoDisplay = false
+                }))
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
     @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
     fun testShouldExcludeCaptionFromAppBounds_resizeable_false() {
@@ -181,6 +193,17 @@
         )
     }
 
+
+    @Test
+    @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
+    @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
+    @EnableCompatChanges(ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS)
+    fun testShouldExcludeCaptionFromAppBounds_resizeable_overridden_true() {
+        assertTrue(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(
+            setUpFreeformTask().apply { isResizeable = true })
+        )
+    }
+
     fun setUpFreeformTask(): TaskInfo =
         createFreeformTask().apply {
             val componentName =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 33f14ac..391d462 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -157,33 +157,33 @@
     }
 
     @Test
-    fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() {
-        doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+    fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() {
+        doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
 
-        assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+        assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @Test
-    fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() {
-        assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
+    fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() {
+        assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @Test
-    fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() {
-        assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
+    fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() {
+        assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @Test
-    fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() {
+    fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() {
         doReturn(true).whenever(mockResources).getBoolean(
             eq(R.bool.config_isDesktopModeDevOptionSupported)
         )
 
-        assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+        assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
     }
 
     @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index b9d6a45..e5a6a6d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -360,7 +360,8 @@
             mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT,
                     mock(SurfaceControl.Transaction.class));
         } else {
-            mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
+            mStageCoordinator.onRecentsInSplitAnimationFinishing(
+                    mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT,
                     mock(SurfaceControl.Transaction.class));
         }
         assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -430,7 +431,8 @@
             mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT,
                     mock(SurfaceControl.Transaction.class));
         } else {
-            mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
+            mStageCoordinator.onRecentsInSplitAnimationFinishing(
+                    mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT,
                     mock(SurfaceControl.Transaction.class));
         }
         assertTrue(mStageCoordinator.isSplitScreenVisible());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
index 82392e0..18fdbef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -48,6 +48,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
@@ -96,7 +97,7 @@
         mTransitionHandler = new DefaultTransitionHandler(
                 mContext, mShellInit, mDisplayController,
                 mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor,
-                mRootTaskDisplayAreaOrganizer);
+                mRootTaskDisplayAreaOrganizer, mock(InteractionJankMonitor.class));
         mShellInit.init();
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
index 53ae967..067dcec 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
@@ -73,7 +73,7 @@
                 .spyStatic(DesktopModeStatus::class.java)
                 .spyStatic(DragPositioningCallbackUtility::class.java)
                 .startMocking()
-        doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+        doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
         doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())}
         setUpCommon()
         whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index f15418ad..da41a23 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -28,7 +28,6 @@
 import android.content.Context
 import android.content.Intent
 import android.content.Intent.ACTION_MAIN
-import android.content.pm.PackageManager
 import android.graphics.Rect
 import android.graphics.Region
 import android.hardware.display.DisplayManager
@@ -116,7 +115,8 @@
                 .spyStatic(DragPositioningCallbackUtility::class.java)
                 .startMocking()
 
-        doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) }
+        doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) }
+        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) }
         doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
 
         setUpCommon()
@@ -309,14 +309,10 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
     fun testDecorationIsNotCreatedForDefaultHomePackage() {
-        val packageManager: PackageManager = org.mockito.kotlin.mock()
-        val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
         val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply {
-            baseActivity = homeActivities
+            baseActivity = homeComponentName
             isTopActivityNoDisplay = false
         }
-        mContext.setMockPackageManager(packageManager)
-        whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
 
         onTaskOpening(task)
 
@@ -384,7 +380,7 @@
         whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
 
         val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
-        doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+        doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) }
         setUpMockDecorationsForTasks(task)
 
         onTaskOpening(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index 9f106da..e40034b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -20,7 +20,9 @@
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.app.WindowConfiguration.WindowingMode
+import android.content.ComponentName
 import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
 import android.graphics.Rect
 import android.hardware.input.InputManager
 import android.os.Handler
@@ -146,8 +148,10 @@
     protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
     protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
     protected val motionEvent = mock<MotionEvent>()
-    val displayLayout = mock<DisplayLayout>()
-    val display = mock<Display>()
+    private val displayLayout = mock<DisplayLayout>()
+    private val display = mock<Display>()
+    private val packageManager = mock<PackageManager>()
+    protected val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
     protected lateinit var spyContext: TestableContext
     private lateinit var desktopModeEventLogger: DesktopModeEventLogger
 
@@ -178,7 +182,7 @@
         whenever(mockDisplayController.getDisplay(any())).thenReturn(display)
         whenever(mockDesktopUserRepositories.getProfile(anyInt()))
             .thenReturn(mockDesktopRepository)
-        desktopModeCompatPolicy = DesktopModeCompatPolicy(context)
+        desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext)
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
             spyContext,
             testShellExecutor,
@@ -273,6 +277,8 @@
         whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
             (i.arguments.first() as Rect).set(STABLE_BOUNDS)
         }
+        spyContext.setMockPackageManager(packageManager)
+        whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(homeComponentName)
     }
 
     @After
@@ -354,5 +360,6 @@
         val STABLE_INSETS = Rect(0, 100, 0, 0)
         val INITIAL_BOUNDS = Rect(0, 0, 100, 100)
         val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+        val HOME_LAUNCHER_PACKAGE_NAME = "com.android.launcher"
     }
 }
diff --git a/libs/hwui/OWNERS b/libs/hwui/OWNERS
index bc17459..70d13ab 100644
--- a/libs/hwui/OWNERS
+++ b/libs/hwui/OWNERS
@@ -4,7 +4,6 @@
 djsollen@google.com
 jreck@google.com
 njawad@google.com
-scroggo@google.com
 sumir@google.com
 
 # For text, e.g. Typeface, Font, Minikin, etc.
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index b1550b0..63a024b 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -260,7 +260,7 @@
 #endif
 
 sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr,
-                                 size_t size, bool readOnly) {
+                                 size_t size, bool readOnly, int64_t id) {
 #ifdef _WIN32 // ashmem not implemented on Windows
      return nullptr;
 #else
@@ -279,7 +279,7 @@
         }
     }
 
-    sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes));
+    sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id));
     if (readOnly) {
         bitmap->setImmutable();
     }
@@ -334,7 +334,7 @@
         : SkPixelRef(info.width(), info.height(), address, rowBytes)
         , mInfo(validateAlpha(info))
         , mPixelStorageType(PixelStorageType::Ashmem)
-        , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) {
+        , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) {
     mPixelStorage.ashmem.address = address;
     mPixelStorage.ashmem.fd = fd;
     mPixelStorage.ashmem.size = mappedSize;
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index 8abe6a8..4e9bcf2 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -97,7 +97,7 @@
                                     BitmapPalette palette);
 #endif
     static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr,
-                                    size_t size, bool readOnly);
+                                    size_t size, bool readOnly, int64_t id);
     static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&);
 
     int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); }
@@ -183,15 +183,15 @@
 
   static bool compress(const SkBitmap& bitmap, JavaCompressFormat format,
                        int32_t quality, SkWStream* stream);
-private:
-    static constexpr uint64_t INVALID_BITMAP_ID = 0u;
 
+    static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u;
+private:
     static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
 
     Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes);
     Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info);
     Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes,
-           uint64_t id = INVALID_BITMAP_ID);
+           uint64_t id = UNDEFINED_BITMAP_ID);
 #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
     Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes,
            BitmapPalette palette);
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index 29efd98..cfde0b2 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -191,9 +191,8 @@
             info.width(), info.height(), isPremultiplied);
 }
 
-jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
-        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
-        int density) {
+jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk,
+                     jobject ninePatchInsets, int density, int64_t id) {
     static jmethodID gBitmap_constructorMethodID =
         GetMethodIDOrDie(env, gBitmap_class,
             "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V");
@@ -208,10 +207,12 @@
     if (!isMutable) {
         bitmapWrapper->bitmap().setImmutable();
     }
+    int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId();
     jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
-            static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper),
-            bitmap->width(), bitmap->height(), density,
-            isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc);
+                                 static_cast<jlong>(bitmapId),
+                                 reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(),
+                                 bitmap->height(), density, isPremultiplied, ninePatchChunk,
+                                 ninePatchInsets, fromMalloc);
 
     if (env->ExceptionCheck() != 0) {
         ALOGE("*** Uncaught exception returned from Java call!\n");
@@ -759,6 +760,7 @@
     const int32_t height = p.readInt32();
     const int32_t rowBytes = p.readInt32();
     const int32_t density = p.readInt32();
+    const int64_t sourceId = p.readInt64();
 
     if (kN32_SkColorType != colorType &&
             kRGBA_F16_SkColorType != colorType &&
@@ -815,7 +817,8 @@
                     return STATUS_NO_MEMORY;
                 }
                 nativeBitmap =
-                        Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable);
+                        Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size,
+                        !isMutable, sourceId);
                 return STATUS_OK;
             });
 
@@ -831,15 +834,15 @@
     }
 
     return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr,
-                        nullptr, density);
+                        nullptr, density, sourceId);
 #else
     jniThrowRuntimeException(env, "Cannot use parcels outside of Android");
     return NULL;
 #endif
 }
 
-static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
-                                     jlong bitmapHandle, jint density, jobject parcel) {
+static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density,
+                                     jobject parcel) {
 #ifdef __ANDROID__ // Layoutlib does not support parcel
     if (parcel == NULL) {
         ALOGD("------- writeToParcel null parcel\n");
@@ -870,6 +873,7 @@
     binder_status_t status;
     int fd = bitmapWrapper->bitmap().getAshmemFd();
     if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) {
+        p.writeInt64(bitmapWrapper->bitmap().getId());
 #if DEBUG_PARCEL
         ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as "
               "immutable blob (fds %s)",
@@ -889,7 +893,7 @@
     ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)",
           p.allowFds() ? "allowed" : "forbidden");
 #endif
-
+    p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID);
     status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap);
     if (status) {
         doThrowRE(env, "Could not copy bitmap to parcel blob.");
diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h
index 21a93f0..c93246a 100644
--- a/libs/hwui/jni/Bitmap.h
+++ b/libs/hwui/jni/Bitmap.h
@@ -18,6 +18,7 @@
 
 #include <jni.h>
 #include <android/bitmap.h>
+#include <hwui/Bitmap.h>
 
 struct SkImageInfo;
 
@@ -33,9 +34,9 @@
     kBitmapCreateFlag_Premultiplied = 0x2,
 };
 
-jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
-            int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr,
-            jobject ninePatchInsets = nullptr, int density = -1);
+jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags,
+                     jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr,
+                     int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID);
 
 Bitmap& toBitmap(jlong bitmapHandle);
 
diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp
index b0f5423..95e4e01 100644
--- a/libs/hwui/jni/ScopedParcel.cpp
+++ b/libs/hwui/jni/ScopedParcel.cpp
@@ -39,6 +39,16 @@
     return temp;
 }
 
+int64_t ScopedParcel::readInt64() {
+    int64_t temp = 0;
+    // TODO: This behavior-matches what android::Parcel does
+    // but this should probably be better
+    if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) {
+        temp = 0;
+    }
+    return temp;
+}
+
 float ScopedParcel::readFloat() {
     float temp = 0.;
     if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) {
diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h
index fd8d6a2..f2f138f 100644
--- a/libs/hwui/jni/ScopedParcel.h
+++ b/libs/hwui/jni/ScopedParcel.h
@@ -35,12 +35,16 @@
 
     uint32_t readUint32();
 
+    int64_t readInt64();
+
     float readFloat();
 
     void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); }
 
     void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); }
 
+    void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); }
+
     void writeFloat(float value) { AParcel_writeFloat(mParcel, value); }
 
     bool allowFds() const { return AParcel_getAllowFds(mParcel); }
diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp
index 747eb8e..5406de8 100644
--- a/libs/input/PointerControllerContext.cpp
+++ b/libs/input/PointerControllerContext.cpp
@@ -15,6 +15,7 @@
  */
 
 #include "PointerControllerContext.h"
+
 #include "PointerController.h"
 
 namespace {
@@ -184,7 +185,7 @@
     DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
     while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
         for (size_t i = 0; i < static_cast<size_t>(n); ++i) {
-            if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
+            if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) {
                 timestamp = buf[i].header.timestamp;
                 gotVsync = true;
             }
diff --git a/media/java/android/media/audiofx/HapticGenerator.java b/media/java/android/media/audiofx/HapticGenerator.java
index d2523ef..7f94dde 100644
--- a/media/java/android/media/audiofx/HapticGenerator.java
+++ b/media/java/android/media/audiofx/HapticGenerator.java
@@ -36,6 +36,20 @@
  * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
  * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio
  * effects.
+ *
+ * <pre>{@code
+ * AudioManager audioManager = context.getSystemService(AudioManager.class);
+ * player = MediaPlayer.create(
+ *         context,
+ *         audioUri,
+ *         new AudioAttributes.Builder().setHapticChannelsMuted(false).build(),
+ *         audioManager.generateAudioSessionId()
+ * );
+ * if (HapticGenerator.isAvailable()) {
+ *     HapticGenerator.create(player.getAudioSessionId()).setEnabled(true);
+ * }
+ * player.start();
+ * }</pre>
  */
 public class HapticGenerator extends AudioEffect implements AutoCloseable {
 
diff --git a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
index e83b9f1..d0de1fc 100644
--- a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -817,7 +817,7 @@
         if (!mOnHost && !autoTransact) {
             return;
         }
-        mAutoTransact.put(pollingLoopFilter, autoTransact);
+        mAutoTransact.put(pollingLoopFilter.toUpperCase(Locale.ROOT), autoTransact);
     }
 
     /**
@@ -845,7 +845,8 @@
         if (!mOnHost && !autoTransact) {
             return;
         }
-        mAutoTransactPatterns.put(Pattern.compile(pollingLoopPatternFilter), autoTransact);
+        mAutoTransactPatterns.put(Pattern.compile(
+                pollingLoopPatternFilter.toUpperCase(Locale.ROOT)), autoTransact);
     }
 
     /**
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index 96e5892..bcc10dd 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -64,7 +64,7 @@
             android:label="@string/u_egg_name"
             android:icon="@drawable/android16_patch_adaptive"
             android:configChanges="orientation|screenLayout|screenSize|density"
-            android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+            android:theme="@style/Theme.Landroid">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml
new file mode 100644
index 0000000..7ac7c38
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
+      android:strokeWidth="2"
+      android:fillColor="#16161D"
+      android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
new file mode 100644
index 0000000..e997b45
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
+      android:strokeWidth="2"
+      android:fillColor="#16161D"
+      android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml
new file mode 100644
index 0000000..4333957
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
+      android:strokeWidth="2"
+      android:fillColor="#16161D"
+      android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
new file mode 100644
index 0000000..c666765
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+      android:strokeWidth="2"
+      android:fillColor="#16161D"
+      android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
new file mode 100644
index 0000000..3cef4ab
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24" android:viewportWidth="24"
+    >
+    <group android:translateX="10" android:translateY="12">
+        <path
+            android:strokeColor="#FFFFFF"
+            android:strokeWidth="2"
+            android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+        "/>
+    </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
new file mode 100644
index 0000000..7a0c703
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24" android:viewportWidth="24"
+    >
+    <group android:translateX="10" android:translateY="12">
+        <path
+            android:strokeColor="#FFFFFF"
+            android:fillColor="#000000"
+            android:strokeWidth="2"
+            android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+        "/>
+    </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
new file mode 100644
index 0000000..2d4ce10
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/ic_spacecraft"
+    android:fromDegrees="0"
+    android:toDegrees="360"
+    />
\ No newline at end of file
diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml
index 5b16304..3a87e45 100644
--- a/packages/EasterEgg/res/values/themes.xml
+++ b/packages/EasterEgg/res/values/themes.xml
@@ -1,7 +1,26 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?><!--
+Copyright (C) 2025 The Android Open Source Project
 
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<resources>
     <style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent="">
         <item name="appWidgetBackgroundColor">@color/light_blue_600</item>
         <item name="appWidgetTextColor">@color/light_blue_50</item>
     </style>
-</resources>
\ No newline at end of file
+
+    <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar">
+        <item name="android:windowLightStatusBar">false</item>
+        <item name="android:windowLightNavigationBar">false</item>
+    </style>
+</resources>
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
index fb5954e..8214c54 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
@@ -41,14 +41,16 @@
 
     val telemetry: String
         get() =
-            listOf(
-                    "---- AUTOPILOT ENGAGED ----",
-                    "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
-                    "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
-                )
-                .joinToString("\n")
+            if (enabled)
+                listOf(
+                        "---- AUTOPILOT ENGAGED ----",
+                        "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
+                        "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
+                    )
+                    .joinToString("\n")
+            else ""
 
-    private var strategy: String = "NONE"
+    var strategy: String = "NONE"
     private var debug: String = ""
 
     override fun update(sim: Simulator, dt: Float) {
@@ -119,7 +121,7 @@
                     target.pos +
                         Vec2.makeWithAngleMag(
                             target.velocity.angle(),
-                            min(altitude / 2, target.velocity.mag())
+                            min(altitude / 2, target.velocity.mag()),
                         )
                 leadingVector = leadingPos - ship.pos
 
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
index d040fba..e748638 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
@@ -20,9 +20,19 @@
 import androidx.compose.animation.core.Easing
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.minimumInteractiveComponentSize
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import kotlin.random.Random
 
 @Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() }
@@ -36,6 +46,40 @@
         animationSpec =
             tween(
                 durationMillis = 1000,
-                easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random)
+                easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
             )
     )
+
+fun flickerFadeInAfterDelay(delay: Int = 0) =
+    fadeIn(
+        animationSpec =
+            tween(
+                durationMillis = 1000,
+                delayMillis = delay,
+                easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
+            )
+    )
+
+@Composable
+fun ConsoleButton(
+    modifier: Modifier = Modifier,
+    textStyle: TextStyle = TextStyle.Default,
+    color: Color,
+    bgColor: Color,
+    borderColor: Color,
+    text: String,
+    onClick: () -> Unit,
+) {
+    Text(
+        style = textStyle,
+        color = color,
+        modifier =
+            modifier
+                .clickable { onClick() }
+                .background(color = bgColor)
+                .border(width = 1.dp, color = borderColor)
+                .padding(6.dp)
+                .minimumInteractiveComponentSize(),
+        text = text,
+    )
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
index d56e8b9..8d4adf6 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
@@ -56,6 +56,8 @@
             }
         }
 
+    private var notifier: UniverseProgressNotifier? = null
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
 
@@ -76,8 +78,8 @@
                         Random.nextFloat() * PI2f,
                         Random.nextFloatInRange(
                             PLANET_ORBIT_RANGE.start,
-                            PLANET_ORBIT_RANGE.endInclusive
-                        )
+                            PLANET_ORBIT_RANGE.endInclusive,
+                        ),
                     )
         }
 
@@ -94,9 +96,11 @@
         composeView.setContent {
             Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
             DebugText(DEBUG_TEXT)
-            Telemetry(universe)
+            Telemetry(universe, showControls = false)
         }
 
+        notifier = UniverseProgressNotifier(this, universe)
+
         composeView.setViewTreeLifecycleOwner(lifecycleOwner)
         composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
 
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 4f77b00..95a60c7 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -21,6 +21,7 @@
 import android.os.Bundle
 import android.util.Log
 import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
 import androidx.compose.animation.AnimatedVisibility
@@ -34,6 +35,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -46,6 +48,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.currentRecomposeScope
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
@@ -59,6 +62,7 @@
 import androidx.compose.ui.graphics.PathEffect
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.text.TextStyle
@@ -74,9 +78,6 @@
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 import java.lang.Float.max
 import java.lang.Float.min
 import java.util.Calendar
@@ -85,11 +86,14 @@
 import kotlin.math.floor
 import kotlin.math.sqrt
 import kotlin.random.Random
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 
 enum class RandomSeedType {
     Fixed,
     Daily,
-    Evergreen
+    Evergreen,
 }
 
 const val TEST_UNIVERSE = false
@@ -138,6 +142,10 @@
         else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "")
     }
 
+fun getSystemDesignation(universe: Universe): String {
+    return "${getDessertCode()}-${universe.randomSeed % 100_000}"
+}
+
 val DEBUG_TEXT = mutableStateOf("Hello Universe")
 const val SHOW_DEBUG_TEXT = false
 
@@ -150,13 +158,13 @@
             fontWeight = FontWeight.Medium,
             fontSize = 9.sp,
             color = Color.Yellow,
-            text = text.value
+            text = text.value,
         )
     }
 }
 
 @Composable
-fun Telemetry(universe: Universe) {
+fun Telemetry(universe: Universe, showControls: Boolean) {
     var topVisible by remember { mutableStateOf(false) }
     var bottomVisible by remember { mutableStateOf(false) }
 
@@ -174,7 +182,6 @@
     LaunchedEffect("blah") {
         delay(1000)
         bottomVisible = true
-        delay(1000)
         topVisible = true
     }
 
@@ -183,13 +190,11 @@
     // TODO: Narrow the scope of invalidation here to the specific data needed;
     // the behavior below mimics the previous implementation of a snapshot ticker value
     val recomposeScope = currentRecomposeScope
-    Telescope(universe) {
-        recomposeScope.invalidate()
-    }
+    Telescope(universe) { recomposeScope.invalidate() }
 
     BoxWithConstraints(
         modifier =
-            Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
+            Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent)
     ) {
         val wide = maxWidth > maxHeight
         Column(
@@ -197,57 +202,82 @@
                 Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart)
                     .fillMaxWidth(if (wide) 0.45f else 1.0f)
         ) {
-            universe.ship.autopilot?.let { autopilot ->
-                if (autopilot.enabled) {
+            val autopilotEnabled = universe.ship.autopilot?.enabled == true
+            if (autopilotEnabled) {
+                universe.ship.autopilot?.let { autopilot ->
                     AnimatedVisibility(
                         modifier = Modifier,
                         visible = bottomVisible,
-                        enter = flickerFadeIn
+                        enter = flickerFadeIn,
                     ) {
                         Text(
                             style = textStyle,
                             color = Colors.Autopilot,
                             modifier = Modifier.align(Left),
-                            text = autopilot.telemetry
+                            text = autopilot.telemetry,
                         )
                     }
                 }
             }
 
-            AnimatedVisibility(
-                modifier = Modifier,
-                visible = bottomVisible,
-                enter = flickerFadeIn
-            ) {
-                Text(
-                    style = textStyle,
-                    color = Colors.Console,
-                    modifier = Modifier.align(Left),
-                    text =
-                        with(universe.ship) {
-                            val closest = universe.closestPlanet()
-                            val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt()
-                            listOfNotNull(
-                                    landing?.let {
-                                        "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}"
-                                    }
-                                        ?: if (distToClosest < 10_000) {
-                                            "ALT: $distToClosest"
-                                        } else null,
-                                    "THR: %.0f%%".format(thrust.mag() * 100f),
-                                    "POS: %s".format(pos.str("%+7.0f")),
-                                    "VEL: %.0f".format(velocity.mag())
-                                )
-                                .joinToString("\n")
+            Row(modifier = Modifier.padding(top = 6.dp)) {
+                AnimatedVisibility(
+                    modifier = Modifier.weight(1f),
+                    visible = bottomVisible,
+                    enter = flickerFadeIn,
+                ) {
+                    Text(
+                        style = textStyle,
+                        color = Colors.Console,
+                        text =
+                            with(universe.ship) {
+                                val closest = universe.closestPlanet()
+                                val distToClosest =
+                                    ((closest.pos - pos).mag() - closest.radius).toInt()
+                                listOfNotNull(
+                                        landing?.let {
+                                            "LND: ${it.planet.name.toUpperCase()}\n" +
+                                                "JOB: ${it.text.toUpperCase()}"
+                                        }
+                                            ?: if (distToClosest < 10_000) {
+                                                "ALT: $distToClosest"
+                                            } else null,
+                                        "THR: %.0f%%".format(thrust.mag() * 100f),
+                                        "POS: %s".format(pos.str("%+7.0f")),
+                                        "VEL: %.0f".format(velocity.mag()),
+                                    )
+                                    .joinToString("\n")
+                            },
+                    )
+                }
+
+                if (showControls) {
+                    AnimatedVisibility(
+                        visible = bottomVisible,
+                        enter = flickerFadeInAfterDelay(500),
+                    ) {
+                        ConsoleButton(
+                            textStyle = textStyle,
+                            color = Colors.Console,
+                            bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent,
+                            borderColor = Colors.Console,
+                            text = "AUTO",
+                        ) {
+                            universe.ship.autopilot?.let {
+                                it.enabled = !it.enabled
+                                DYNAMIC_ZOOM = it.enabled
+                                if (!it.enabled) universe.ship.thrust = Vec2.Zero
+                            }
                         }
-                )
+                    }
+                }
             }
         }
 
         AnimatedVisibility(
             modifier = Modifier.align(Alignment.TopStart),
             visible = topVisible,
-            enter = flickerFadeIn
+            enter = flickerFadeInAfterDelay(1000),
         ) {
             Text(
                 style = textStyle,
@@ -263,13 +293,12 @@
                 text =
                     (with(universe.star) {
                             listOf(
-                                "  STAR: $name (${getDessertCode()}-" +
-                                    "${universe.randomSeed % 100_000})",
+                                "  STAR: $name (${getSystemDesignation(universe)})",
                                 " CLASS: ${cls.name}",
                                 "RADIUS: ${radius.toInt()}",
                                 "  MASS: %.3g".format(mass),
                                 "BODIES: ${explored.size} / ${universe.planets.size}",
-                                ""
+                                "",
                             )
                         } +
                             explored
@@ -280,11 +309,11 @@
                                         "  ATMO: ${it.atmosphere.capitalize()}",
                                         " FAUNA: ${it.fauna.capitalize()}",
                                         " FLORA: ${it.flora.capitalize()}",
-                                        ""
+                                        "",
                                     )
                                 }
                                 .flatten())
-                        .joinToString("\n")
+                        .joinToString("\n"),
 
                 // TODO: different colors, highlight latest discovery
             )
@@ -293,6 +322,7 @@
 }
 
 class MainActivity : ComponentActivity() {
+    private var notifier: UniverseProgressNotifier? = null
     private var foldState = mutableStateOf<FoldingFeature?>(null)
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -300,7 +330,7 @@
 
         onWindowLayoutInfoChange()
 
-        enableEdgeToEdge()
+        enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb()))
 
         val universe = Universe(namer = Namer(resources), randomSeed = randomSeed())
 
@@ -312,12 +342,13 @@
 
         com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext)
 
-        // for autopilot testing in the activity
-        //        val autopilot = Autopilot(universe.ship, universe)
-        //        universe.ship.autopilot = autopilot
-        //        universe.add(autopilot)
-        //        autopilot.enabled = true
-        //        DYNAMIC_ZOOM = autopilot.enabled
+        // set up the autopilot in case we need it
+        val autopilot = Autopilot(universe.ship, universe)
+        universe.ship.autopilot = autopilot
+        universe.add(autopilot)
+        autopilot.enabled = false
+
+        notifier = UniverseProgressNotifier(this, universe)
 
         setContent {
             Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
@@ -329,7 +360,7 @@
                 modifier = Modifier.fillMaxSize(),
                 minRadius = minRadius,
                 maxRadius = maxRadius,
-                color = Color.Green
+                color = Color.Green,
             ) { vec ->
                 (universe.follow as? Spacecraft)?.let { ship ->
                     if (vec == Vec2.Zero) {
@@ -346,13 +377,13 @@
                             ship.thrust =
                                 Vec2.makeWithAngleMag(
                                     a,
-                                    lexp(minRadius, maxRadius, m).coerceIn(0f, 1f)
+                                    lexp(minRadius, maxRadius, m).coerceIn(0f, 1f),
                                 )
                         }
                     }
                 }
             }
-            Telemetry(universe)
+            Telemetry(universe, true)
         }
     }
 
@@ -382,7 +413,7 @@
 
     Spaaaace(modifier = Modifier.fillMaxSize(), universe)
     DebugText(DEBUG_TEXT)
-    Telemetry(universe)
+    Telemetry(universe, true)
 }
 
 @Composable
@@ -391,7 +422,7 @@
     minRadius: Float = 0f,
     maxRadius: Float = 1000f,
     color: Color = Color.Green,
-    onStickChanged: (vector: Vec2) -> Unit
+    onStickChanged: (vector: Vec2) -> Unit,
 ) {
     val origin = remember { mutableStateOf(Vec2.Zero) }
     val target = remember { mutableStateOf(Vec2.Zero) }
@@ -444,14 +475,14 @@
                                             PathEffect.dashPathEffect(
                                                 floatArrayOf(this.density * 1f, this.density * 2f)
                                             )
-                                        else null
-                                )
+                                        else null,
+                                ),
                         )
                         drawLine(
                             color = color,
                             start = origin.value,
                             end = origin.value + Vec2.makeWithAngleMag(a, mag),
-                            strokeWidth = 2f
+                            strokeWidth = 2f,
                         )
                     }
                 }
@@ -462,15 +493,13 @@
 fun Spaaaace(
     modifier: Modifier,
     u: Universe,
-    foldState: MutableState<FoldingFeature?> = mutableStateOf(null)
+    foldState: MutableState<FoldingFeature?> = mutableStateOf(null),
 ) {
     LaunchedEffect(u) {
-        while (true) withInfiniteAnimationFrameNanos { frameTimeNanos ->
-            u.step(frameTimeNanos)
-        }
+        while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) }
     }
 
-    var cameraZoom by remember { mutableStateOf(1f) }
+    var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) }
     var cameraOffset by remember { mutableStateOf(Offset.Zero) }
 
     val transformableState =
@@ -501,15 +530,16 @@
         val closest = u.closestPlanet()
         val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f)
         //        val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f
-        if (DYNAMIC_ZOOM) {
-            cameraZoom =
-                expSmooth(
-                    cameraZoom,
-                    clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM),
-                    dt = u.dt,
-                    speed = 1.5f
-                )
-        } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
+        val targetZoom =
+            if (DYNAMIC_ZOOM) {
+                clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+            } else {
+                DEFAULT_CAMERA_ZOOM
+            }
+        if (!TOUCH_CAMERA_ZOOM) {
+            cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f)
+        }
+
         if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
 
         // cameraZoom: metersToPixels
@@ -521,9 +551,9 @@
                 -cameraOffset -
                     Offset(
                         visibleSpaceSizeMeters.width * centerFracX,
-                        visibleSpaceSizeMeters.height * centerFracY
+                        visibleSpaceSizeMeters.height * centerFracY,
                     ),
-                visibleSpaceSizeMeters
+                visibleSpaceSizeMeters,
             )
 
         var gridStep = 1000f
@@ -537,14 +567,14 @@
                 "fps: ${"%3.0f".format(1f / u.dt)} " +
                 "dt: ${u.dt}\n" +
                 ((u.follow as? Spacecraft)?.let {
-                    "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format(
-                        it.pos.str("%+7.1f"),
-                        it.velocity.mag(),
-                        it.angle,
-                        it.thrust.str("%+5.2f")
-                    )
-                }
-                    ?: "") +
+                    "ship: p=%s v=%7.2f a=%6.3f t=%s\n"
+                        .format(
+                            it.pos.str("%+7.1f"),
+                            it.velocity.mag(),
+                            it.angle,
+                            it.thrust.str("%+5.2f"),
+                        )
+                } ?: "") +
                 "star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " +
                 "class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" +
                 "planets: ${u.planets.size}\n" +
@@ -574,7 +604,7 @@
 
             translate(
                 -visibleSpaceRectMeters.center.x + size.width * 0.5f,
-                -visibleSpaceRectMeters.center.y + size.height * 0.5f
+                -visibleSpaceRectMeters.center.y + size.height * 0.5f,
             ) {
                 // debug outer frame
                 // drawRect(
@@ -590,7 +620,7 @@
                         color = Colors.Eigengrau2,
                         start = Offset(x, visibleSpaceRectMeters.top),
                         end = Offset(x, visibleSpaceRectMeters.bottom),
-                        strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+                        strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
                     )
                     x += gridStep
                 }
@@ -601,7 +631,7 @@
                         color = Colors.Eigengrau2,
                         start = Offset(visibleSpaceRectMeters.left, y),
                         end = Offset(visibleSpaceRectMeters.right, y),
-                        strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+                        strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
                     )
                     y += gridStep
                 }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
index 7331807..babf132 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -16,8 +16,8 @@
 
 package com.android.egg.landroid
 
-import android.content.res.Resources
 import com.android.egg.R
+import android.content.res.Resources
 import kotlin.random.Random
 
 const val SUFFIX_PROB = 0.75f
@@ -58,7 +58,7 @@
             1f to "*",
             1f to "^",
             1f to "#",
-            0.1f to "(^*!%@##!!"
+            0.1f to "(^*!%@##!!",
         )
 
     private var activities = Bag(resources.getStringArray(R.array.activities))
@@ -101,26 +101,26 @@
     fun floraPlural(rng: Random): String {
         return floraGenericPlurals.pull(rng)
     }
+
     fun faunaPlural(rng: Random): String {
         return faunaGenericPlurals.pull(rng)
     }
+
     fun atmoPlural(rng: Random): String {
         return atmoGenericPlurals.pull(rng)
     }
 
     val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""")
+
     fun describeActivity(rng: Random, target: Planet?): String {
-        return activities
-            .pull(rng)
-            .replace(TEMPLATE_REGEX) {
-                when (it.groupValues[1]) {
-                    "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
-                    "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
-                    "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
-                    "planet" -> (target?.description ?: "SOME BODY") // once told me
-                    else -> "unknown template tag: ${it.groupValues[0]}"
-                }
+        return activities.pull(rng).replace(TEMPLATE_REGEX) {
+            when (it.groupValues[1]) {
+                "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
+                "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
+                "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
+                "planet" -> (target?.description ?: "SOME BODY") // once told me
+                else -> "unknown template tag: ${it.groupValues[0]}"
             }
-            .toUpperCase()
+        }
     }
 }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
new file mode 100644
index 0000000..bb3a04d
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.egg.landroid
+
+import com.android.egg.R
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.util.lerp
+import kotlinx.coroutines.DisposableHandle
+
+const val CHANNEL_ID = "progress"
+const val CHANNEL_NAME = "Spacecraft progress"
+const val UPDATE_FREQUENCY_SEC = 1f
+
+fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float =
+    lerp(range.start, range.endInclusive, x)
+
+class UniverseProgressNotifier(val context: Context, val universe: Universe) {
+    private val notificationId = universe.randomSeed.toInt()
+    private val chan =
+        NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
+            .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC }
+    private val noman =
+        context.getSystemService(NotificationManager::class.java)?.apply {
+            createNotificationChannel(chan)
+        }
+
+    private val registration: DisposableHandle =
+        universe.addSimulationStepListener(this::onSimulationStep)
+
+    private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled)
+    private val planetIcons =
+        listOf(
+            (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to
+                Icon.createWithResource(context, R.drawable.ic_planet_large),
+            (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to
+                Icon.createWithResource(context, R.drawable.ic_planet_medium),
+            (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to
+                Icon.createWithResource(context, R.drawable.ic_planet_small),
+            (PLANET_RADIUS_RANGE.start to
+                Icon.createWithResource(context, R.drawable.ic_planet_tiny)),
+        )
+
+    private fun getPlanetIcon(planet: Planet): Icon {
+        for ((radius, icon) in planetIcons) {
+            if (planet.radius > radius) return icon
+        }
+        return planetIcons.last().second
+    }
+
+    private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon)
+
+    private val builder =
+        Notification.Builder(context, CHANNEL_ID)
+            .setContentIntent(
+                PendingIntent.getActivity(
+                    context,
+                    0,
+                    Intent(context, MainActivity::class.java).apply {
+                        flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+                    },
+                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+                )
+            )
+            .setPriority(Notification.PRIORITY_DEFAULT)
+            .setColorized(true)
+            .setOngoing(true)
+            .setColor(Colors.Eigengrau2.toArgb())
+            .setStyle(progress)
+
+    private var lastUpdate = 0f
+    private var initialDistToTarget = 0
+
+    private fun onSimulationStep() {
+        if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) {
+            lastUpdate = universe.now
+            // android.util.Log.v("Landroid", "posting notification at time ${universe.now}")
+
+            var distToTarget = 0
+            val autopilot = universe.ship.autopilot
+            val autopilotEnabled: Boolean = autopilot?.enabled == true
+            val target = autopilot?.target
+            val landing = universe.ship.landing
+            val speed = universe.ship.velocity.mag()
+
+            if (landing != null) {
+                // landed
+                builder.setContentTitle("landed: ${landing.planet.name}")
+                builder.setContentText("currently: ${landing.text}")
+                builder.setShortCriticalText("landed")
+
+                progress.setProgress(progress.progressMax)
+                progress.setProgressIndeterminate(false)
+
+                builder.setStyle(progress)
+            } else if (autopilotEnabled) {
+                if (target != null) {
+                    // autopilot en route
+                    distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt()
+                    if (initialDistToTarget == 0) {
+                        // we have a new target!
+                        initialDistToTarget = distToTarget
+                        progress.progressEndIcon = getPlanetIcon(target)
+                    }
+
+                    val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???"
+                    builder.setContentTitle("headed to: ${target.name}")
+                    builder.setContentText(
+                        "autopilot is ${autopilot.strategy.toLowerCase()}" +
+                            "\ndist: ${distToTarget}u // eta: $eta"
+                    )
+                    // fun fact: ProgressStyle was originally EnRouteStyle
+                    builder.setShortCriticalText("en route")
+
+                    progress
+                        .setProgressSegments(
+                            listOf(
+                                Notification.ProgressStyle.Segment(initialDistToTarget)
+                                    .setColor(Colors.Track.toArgb())
+                            )
+                        )
+                        .setProgress(initialDistToTarget - distToTarget)
+                        .setProgressIndeterminate(false)
+                    builder.setStyle(progress)
+                } else {
+                    // no target
+                    if (initialDistToTarget != 0) {
+                        // just launched
+                        initialDistToTarget = 0
+                        progress.progressStartIcon = progress.progressEndIcon
+                        progress.progressEndIcon = null
+                    }
+
+                    builder.setContentTitle("in space")
+                    builder.setContentText("selecting new target...")
+                    builder.setShortCriticalText("launched")
+
+                    progress.setProgressIndeterminate(true)
+
+                    builder.setStyle(progress)
+                }
+            } else {
+                // under user control
+
+                initialDistToTarget = 0
+
+                builder.setContentTitle("in space")
+                builder.setContentText("under manual control")
+                builder.setShortCriticalText("adrift")
+
+                builder.setStyle(null)
+            }
+
+            builder
+                .setSubText(getSystemDesignation(universe))
+                .setSmallIcon(R.drawable.ic_spacecraft_rotated)
+
+            val notification = builder.build()
+
+            // one of the silliest things about Android is that icon levels go from 0 to 10000
+            notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt()
+
+            noman?.notify(notificationId, notification)
+        }
+    }
+}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm
new file mode 100644
index 0000000..0059d00
--- /dev/null
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm
@@ -0,0 +1,400 @@
+# Copyright 2025 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# English (India) keyboard layout.
+#
+
+type OVERLAY
+
+map key 86 PLUS
+
+### ROW 1
+
+key GRAVE {
+    label:                              '`'
+    base:                               '`'
+    shift:                              '~'
+    ralt:                               '\u0300'
+    ralt+shift:                         '\u0303'
+}
+
+key 1 {
+    label:                              '1'
+    base:                               '1'
+    shift:                              '!'
+}
+
+key 2 {
+    label:                              '2'
+    base:                               '2'
+    shift:                              '@'
+}
+
+key 3 {
+    label:                              '3'
+    base:                               '3'
+    shift:                              '#'
+}
+
+key 4 {
+    label:                              '4'
+    base:                               '4'
+    shift:                              '$'
+    ralt, ctrl+shift:                   '\u20b9'
+}
+
+key 5 {
+    label:                              '5'
+    base:                               '5'
+    shift:                              '%'
+}
+
+key 6 {
+    label:                              '6'
+    base:                               '6'
+    shift:                              '^'
+    ralt+shift:                         '\u0302'
+}
+
+key 7 {
+    label:                              '7'
+    base:                               '7'
+    shift:                              '&'
+}
+
+key 8 {
+    label:                              '8'
+    base:                               '8'
+    shift:                              '*'
+}
+
+key 9 {
+    label:                              '9'
+    base:                               '9'
+    shift:                              '('
+    ralt+shift:                         '\u0306'
+}
+
+key 0 {
+    label:                              '0'
+    base:                               '0'
+    shift:                              ')'
+}
+
+key MINUS {
+    label:                              '-'
+    base:                               '-'
+    shift:                              '_'
+    ralt+shift:                         '\u0331'
+}
+
+key EQUALS {
+    label:                              '='
+    base:                               '='
+    shift:                              '+'
+    ralt:                               '\u2013'
+    ralt+shift:                         '\u2014'
+}
+
+### ROW 2
+
+key Q {
+    label:                              'Q'
+    base:                               'q'
+    shift, capslock:                    'Q'
+    capslock+shift:                     'q'
+    ralt:                               '\u00e6'
+    ralt+shift, ralt+capslock:          '\u00c6'
+    ralt+shift+capslock:                '\u00e6'
+}
+
+key W {
+    label:                              'W'
+    base:                               'w'
+    shift, capslock:                    'W'
+    capslock+shift:                     'w'
+}
+
+key E {
+    label:                              'E'
+    base:                               'e'
+    shift, capslock:                    'E'
+    capslock+shift:                     'e'
+    ralt:                               '\u0113'
+    ralt+shift, ralt+capslock:          '\u0112'
+    ralt+shift+capslock:                '\u0113'
+}
+
+key R {
+    label:                              'R'
+    base:                               'r'
+    shift, capslock:                    'R'
+    capslock+shift:                     'r'
+}
+
+key T {
+    label:                              'T'
+    base:                               't'
+    shift, capslock:                    'T'
+    capslock+shift:                     't'
+    ralt:                               '\u1e6d'
+    ralt+shift, ralt+capslock:          '\u1e6c'
+    ralt+shift+capslock:                '\u1e6d'
+}
+
+key Y {
+    label:                              'Y'
+    base:                               'y'
+    shift, capslock:                    'Y'
+    capslock+shift:                     'y'
+    ralt:                               '\u00f1'
+    ralt+shift, ralt+capslock:          '\u00d1'
+    ralt+shift+capslock:                '\u00f1'
+}
+
+key U {
+    label:                              'U'
+    base:                               'u'
+    shift, capslock:                    'U'
+    capslock+shift:                     'u'
+    ralt:                               '\u016b'
+    ralt+shift, ralt+capslock:          '\u016a'
+    ralt+shift+capslock:                '\u016b'
+}
+
+key I {
+    label:                              'I'
+    base:                               'i'
+    shift, capslock:                    'I'
+    capslock+shift:                     'i'
+    ralt:                               '\u012b'
+    ralt+shift, ralt+capslock:          '\u012a'
+    ralt+shift+capslock:                '\u012b'
+}
+
+key O {
+    label:                              'O'
+    base:                               'o'
+    shift, capslock:                    'O'
+    capslock+shift:                     'o'
+    ralt:                               '\u014d'
+    ralt+shift, ralt+capslock:          '\u014c'
+    ralt+shift+capslock:                '\u014d'
+}
+
+key P {
+    label:                              'P'
+    base:                               'p'
+    shift, capslock:                    'P'
+    capslock+shift:                     'p'
+}
+
+key LEFT_BRACKET {
+    label:                              '['
+    base:                               '['
+    shift:                              '{'
+}
+
+key RIGHT_BRACKET {
+    label:                              ']'
+    base:                               ']'
+    shift:                              '}'
+}
+
+### ROW 3
+
+key A {
+    label:                              'A'
+    base:                               'a'
+    shift, capslock:                    'A'
+    capslock+shift:                     'a'
+    ralt:                               '\u0101'
+    ralt+shift, ralt+capslock:          '\u0100'
+    ralt+shift+capslock:                '\u0101'
+}
+
+key S {
+    label:                              'S'
+    base:                               's'
+    shift, capslock:                    'S'
+    capslock+shift:                     's'
+    ralt:                               '\u015b'
+    ralt+shift, ralt+capslock:          '\u015a'
+    ralt+shift+capslock:                '\u015b'
+}
+
+key D {
+    label:                              'D'
+    base:                               'd'
+    shift, capslock:                    'D'
+    capslock+shift:                     'd'
+    ralt:                               '\u1e0d'
+    ralt+shift, ralt+capslock:          '\u1e0c'
+    ralt+shift+capslock:                 '\u1e0d'
+}
+
+key F {
+    label:                              'F'
+    base:                               'f'
+    shift, capslock:                    'F'
+    capslock+shift:                     'f'
+}
+
+key G {
+    label:                              'G'
+    base:                               'g'
+    shift, capslock:                    'G'
+    capslock+shift:                     'g'
+    ralt:                               '\u1e45'
+    ralt+shift, ralt+capslock:          '\u1e44'
+    ralt+shift+capslock:                '\u1e45'
+}
+
+key H {
+    label:                              'H'
+    base:                               'h'
+    shift, capslock:                    'H'
+    capslock+shift:                     'h'
+    ralt:                               '\u1e25'
+    ralt+shift, ralt+capslock:          '\u1e24'
+    ralt+shift+capslock:                '\u1e25'
+}
+
+key J {
+    label:                              'J'
+    base:                               'j'
+    shift, capslock:                    'J'
+    capslock+shift:                     'j'
+}
+
+key K {
+    label:                              'K'
+    base:                               'k'
+    shift, capslock:                    'K'
+    capslock+shift:                     'k'
+}
+
+key L {
+    label:                              'L'
+    base:                               'l'
+    shift, capslock:                    'L'
+    capslock+shift:                     'l'
+}
+
+key SEMICOLON {
+    label:                              ';'
+    base:                               ';'
+    shift:                              ':'
+}
+
+key APOSTROPHE {
+    label:                              '\''
+    base:                               '\''
+    shift:                              '\u0022'
+    ralt:                               '\u030d'
+    ralt+shift:                         '\u030e'
+}
+
+key BACKSLASH {
+    label:                              '\\'
+    base:                               '\\'
+    shift:                              '|'
+}
+
+### ROW 4
+
+key PLUS {
+    label:                              '\\'
+    base:                               '\\'
+    shift:                              '|'
+}
+
+key Z {
+    label:                              'Z'
+    base:                               'z'
+    shift, capslock:                    'Z'
+    capslock+shift:                     'z'
+}
+
+key X {
+    label:                              'X'
+    base:                               'x'
+    shift, capslock:                    'X'
+    capslock+shift:                     'x'
+    ralt:                               '\u1e63'
+    ralt+shift, ralt+capslock:          '\u1e62'
+    ralt+shift+capslock:                '\u1e63'
+}
+
+key C {
+    label:                              'C'
+    base:                               'c'
+    shift, capslock:                    'C'
+    capslock+shift:                     'c'
+}
+
+key V {
+    label:                              'V'
+    base:                               'v'
+    shift, capslock:                    'V'
+    capslock+shift:                     'v'
+}
+
+key B {
+    label:                              'B'
+    base:                               'b'
+    shift, capslock:                    'B'
+    capslock+shift:                     'b'
+}
+
+key N {
+    label:                              'N'
+    base:                               'n'
+    shift, capslock:                    'N'
+    capslock+shift:                     'n'
+    ralt:                               '\u1e47'
+    ralt+shift, ralt+capslock:          '\u1e46'
+    ralt+shift+capslock:                '\u1e47'
+}
+
+key M {
+    label:                              'M'
+    base:                               'm'
+    shift, capslock:                    'M'
+    capslock+shift:                     'm'
+    ralt:                               '\u1e41'
+    ralt+shift, ralt+capslock:          '\u1e40'
+    ralt+shift+capslock:                '\u1e41'
+}
+
+key COMMA {
+    label:                              ','
+    base:                               ','
+    shift:                              '<'
+    ralt+shift:                         '\u030C'
+}
+
+key PERIOD {
+    label:                              '.'
+    base:                               '.'
+    shift:                              '>'
+    ralt:                               '\u0323'
+}
+
+key SLASH {
+    label:                              '/'
+    base:                               '/'
+    shift:                              '?'
+}
diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml
index bd7cdc4..8a397a5 100644
--- a/packages/InputDevices/res/values/strings.xml
+++ b/packages/InputDevices/res/values/strings.xml
@@ -167,4 +167,7 @@
 
     <!-- Romanian keyboard layout label. [CHAR LIMIT=35] -->
     <string name="keyboard_layout_romanian">Romanian</string>
+
+    <!-- English (India) keyboard layout label. [CHAR LIMIT=35] -->
+    <string name="keyboard_layout_english_india">English (India)</string>
 </resources>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 9ce9a87..fa0ed13 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -367,4 +367,11 @@
         android:keyboardLayout="@raw/keyboard_layout_romanian"
         android:keyboardLocale="ro-Latn-RO"
         android:keyboardLayoutType="qwerty" />
+
+    <keyboard-layout
+        android:name="keyboard_layout_english_india"
+        android:label="@string/keyboard_layout_english_india"
+        android:keyboardLayout="@raw/keyboard_layout_english_india"
+        android:keyboardLocale="en-Latn-IN"
+        android:keyboardLayoutType="qwerty" />
 </keyboard-layouts>
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
index ec287c1..52a2160 100644
--- a/packages/SettingsLib/Graph/graph.proto
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -95,6 +95,8 @@
   optional PermissionsProto write_permissions = 18;
   // Tag constants associated with the preference.
   repeated string tags = 19;
+  // Permit to read and write preference value (the lower 15 bits is reserved for read permit).
+  optional int32 read_write_permit = 20;
 
   // Target of an Intent
   message ActionTarget {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index e511bf1..13541b1 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -56,6 +56,8 @@
 import com.android.settingslib.metadata.PreferenceSummaryProvider
 import com.android.settingslib.metadata.PreferenceTitleProvider
 import com.android.settingslib.metadata.ReadWritePermit
+import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
+import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
 import com.android.settingslib.preference.PreferenceScreenFactory
 import com.android.settingslib.preference.PreferenceScreenProvider
 import java.util.Locale
@@ -415,52 +417,46 @@
         for (tag in metadata.tags(context)) addTags(tag)
     }
     persistent = metadata.isPersistent(context)
-    if (persistent) {
-        if (metadata is PersistentPreference<*>) {
-            sensitivityLevel = metadata.sensitivityLevel
-            metadata.getReadPermissions(context)?.let {
-                if (it.size > 0) readPermissions = it.toProto()
-            }
-            metadata.getWritePermissions(context)?.let {
-                if (it.size > 0) writePermissions = it.toProto()
+    if (metadata !is PersistentPreference<*>) return@preferenceProto
+    sensitivityLevel = metadata.sensitivityLevel
+    metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() }
+    metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() }
+    val readPermit = metadata.evalReadPermit(context, callingPid, callingUid)
+    val writePermit =
+        metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW
+    readWritePermit = ReadWritePermit.make(readPermit, writePermit)
+    if (
+        flags.includeValue() &&
+            enabled &&
+            (!hasAvailable() || available) &&
+            (!hasRestricted() || !restricted) &&
+            readPermit == ReadWritePermit.ALLOW
+    ) {
+        val storage = metadata.storage(context)
+        value = preferenceValueProto {
+            when (metadata.valueType) {
+                Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it }
+                Boolean::class.javaObjectType ->
+                    storage.getBoolean(metadata.key)?.let { booleanValue = it }
+                Float::class.javaObjectType ->
+                    storage.getFloat(metadata.key)?.let { floatValue = it }
+                else -> {}
             }
         }
-        if (
-            flags.includeValue() &&
-                enabled &&
-                (!hasAvailable() || available) &&
-                (!hasRestricted() || !restricted) &&
-                metadata is PersistentPreference<*> &&
-                metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW
-        ) {
-            val storage = metadata.storage(context)
-            value = preferenceValueProto {
-                when (metadata.valueType) {
-                    Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it }
-                    Boolean::class.javaObjectType ->
-                        storage.getBoolean(metadata.key)?.let { booleanValue = it }
-                    Float::class.javaObjectType ->
-                        storage.getFloat(metadata.key)?.let { floatValue = it }
-                    else -> {}
-                }
-            }
-        }
-        if (flags.includeValueDescriptor()) {
-            valueDescriptor = preferenceValueDescriptorProto {
-                when (metadata) {
-                    is IntRangeValuePreference -> rangeValue = rangeValueProto {
-                            min = metadata.getMinValue(context)
-                            max = metadata.getMaxValue(context)
-                            step = metadata.getIncrementStep(context)
-                        }
-                    else -> {}
-                }
-                if (metadata is PersistentPreference<*>) {
-                    when (metadata.valueType) {
-                        Boolean::class.javaObjectType -> booleanType = true
-                        Float::class.javaObjectType -> floatType = true
+    }
+    if (flags.includeValueDescriptor()) {
+        valueDescriptor = preferenceValueDescriptorProto {
+            when (metadata) {
+                is IntRangeValuePreference -> rangeValue = rangeValueProto {
+                        min = metadata.getMinValue(context)
+                        max = metadata.getMaxValue(context)
+                        step = metadata.getIncrementStep(context)
                     }
-                }
+                else -> {}
+            }
+            when (metadata.valueType) {
+                Boolean::class.javaObjectType -> booleanType = true
+                Float::class.javaObjectType -> floatType = true
             }
         }
     }
@@ -478,6 +474,20 @@
         else -> getReadPermit(context, callingPid, callingUid)
     }
 
+/** Evaluates the write permit of a persistent preference. */
+fun <T> PersistentPreference<T>.evalWritePermit(
+    context: Context,
+    callingPid: Int,
+    callingUid: Int,
+): Int? =
+    when {
+        sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
+            ReadWritePermit.DISALLOW
+        getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
+            ReadWritePermit.REQUIRE_APP_PERMISSION
+        else -> getWritePermit(context, callingPid, callingUid)
+    }
+
 private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? {
     if (isRoot && this is PreferenceScreenMetadata) {
         val titleRes = screenTitle
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index 60f9c6b..72f6934 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -36,8 +36,6 @@
 import com.android.settingslib.metadata.PreferenceRestrictionProvider
 import com.android.settingslib.metadata.PreferenceScreenRegistry
 import com.android.settingslib.metadata.ReadWritePermit
-import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
-import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
 
 /** Request to set preference value. */
 class PreferenceSetterRequest(
@@ -223,13 +221,8 @@
     callingPid: Int,
     callingUid: Int,
 ): Int =
-    when {
-        sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
-            ReadWritePermit.DISALLOW
-        getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
-            ReadWritePermit.REQUIRE_APP_PERMISSION
-        else -> getWritePermit(context, value, callingPid, callingUid)
-    }
+    evalWritePermit(context, callingPid, callingUid)
+        ?: getWritePermit(context, value, callingPid, callingUid)
 
 /** Message codec for [PreferenceSetterRequest]. */
 object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> {
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
index e456a7f..c723dce 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -41,6 +41,19 @@
         const val REQUIRE_APP_PERMISSION = 2
         /** Require explicit user agreement (e.g. terms of service). */
         const val REQUIRE_USER_AGREEMENT = 3
+
+        private const val READ_PERMIT_BITS = 15
+        private const val READ_PERMIT_MASK = (1 shl 16) - 1
+
+        /** Wraps given read and write permit into an integer. */
+        fun make(readPermit: @ReadWritePermit Int, writePermit: @ReadWritePermit Int): Int =
+            (writePermit shl READ_PERMIT_BITS) or readPermit
+
+        /** Extracts the read permit from given integer generated by [make]. */
+        fun getReadPermit(readWritePermit: Int): Int = readWritePermit and READ_PERMIT_MASK
+
+        /** Extracts the write permit from given integer generated by [make]. */
+        fun getWritePermit(readWritePermit: Int): Int = readWritePermit shr READ_PERMIT_BITS
     }
 }
 
@@ -81,6 +94,12 @@
     /** The value type the preference is associated with. */
     val valueType: Class<T>
 
+    /** The sensitivity level of the preference. */
+    val sensitivityLevel: @SensitivityLevel Int
+        get() = SensitivityLevel.UNKNOWN_SENSITIVITY
+
+    override fun isPersistent(context: Context) = true
+
     /**
      * Returns the key-value storage of the preference.
      *
@@ -102,19 +121,27 @@
      * behind the scene.
      */
     fun getReadPermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int =
-        PreferenceScreenRegistry.getReadPermit(
-            context,
-            callingPid,
-            callingUid,
-            this,
-        )
+        PreferenceScreenRegistry.defaultReadPermit
 
     /** Returns the required permissions to write preference value. */
     fun getWritePermissions(context: Context): Permissions? = null
 
     /**
      * Returns if the external application (identified by [callingPid] and [callingUid]) is
-     * permitted to write preference with given [value].
+     * permitted to write preference value. If the write permit depends on certain value, implement
+     * the overloading [getWritePermit] instead.
+     *
+     * The underlying implementation does NOT need to check common states like isEnabled,
+     * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it
+     * behind the scene.
+     */
+    fun getWritePermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int? =
+        null
+
+    /**
+     * Returns if the external application (identified by [callingPid] and [callingUid]) is
+     * permitted to write preference with given [value]. Note that if the overloading
+     * [getWritePermit] returns non null value, this method will be ignored!
      *
      * The underlying implementation does NOT need to check common states like isEnabled,
      * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it
@@ -125,18 +152,7 @@
         value: T?,
         callingPid: Int,
         callingUid: Int,
-    ): @ReadWritePermit Int =
-        PreferenceScreenRegistry.getWritePermit(
-            context,
-            value,
-            callingPid,
-            callingUid,
-            this,
-        )
-
-    /** The sensitivity level of the preference. */
-    val sensitivityLevel: @SensitivityLevel Int
-        get() = SensitivityLevel.UNKNOWN_SENSITIVITY
+    ): @ReadWritePermit Int = PreferenceScreenRegistry.defaultWritePermit
 }
 
 /** Descriptor of values. */
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
index a8939ab..7f2a610 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -127,7 +127,7 @@
     fun dependencies(context: Context): Array<String> = arrayOf()
 
     /** Returns if the preference is persistent in datastore. */
-    fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+    fun isPersistent(context: Context): Boolean = false
 
     /**
      * Returns if preference value backup is allowed (by default returns `true` if preference is
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
index 2463109..8d4bfff 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -22,12 +22,18 @@
 import com.android.settingslib.datastore.KeyValueStore
 
 /** Registry of all available preference screens in the app. */
-object PreferenceScreenRegistry : ReadWritePermitProvider {
+object PreferenceScreenRegistry {
     private const val TAG = "ScreenRegistry"
 
     /** Provider of key-value store. */
     private lateinit var keyValueStoreProvider: KeyValueStoreProvider
 
+    /** The default permit for external application to read preference values. */
+    var defaultReadPermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW
+
+    /** The default permit for external application to write preference values. */
+    var defaultWritePermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW
+
     /**
      * Factories of all available [PreferenceScreenMetadata]s.
      *
@@ -38,9 +44,6 @@
     /** Metrics logger for preference actions triggered by user interaction. */
     var preferenceUiActionMetricsLogger: PreferenceUiActionMetricsLogger? = null
 
-    private var readWritePermitProvider: ReadWritePermitProvider =
-        object : ReadWritePermitProvider {}
-
     /** Sets the [KeyValueStoreProvider]. */
     fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
         this.keyValueStoreProvider = keyValueStoreProvider
@@ -77,28 +80,6 @@
             return null
         }
     }
-
-    /**
-     * Sets the provider to check read write permit. Read and write requests are denied by default.
-     */
-    fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider) {
-        this.readWritePermitProvider = readWritePermitProvider
-    }
-
-    override fun getReadPermit(
-        context: Context,
-        callingPid: Int,
-        callingUid: Int,
-        preference: PreferenceMetadata,
-    ) = readWritePermitProvider.getReadPermit(context, callingPid, callingUid, preference)
-
-    override fun getWritePermit(
-        context: Context,
-        value: Any?,
-        callingPid: Int,
-        callingUid: Int,
-        preference: PreferenceMetadata,
-    ) = readWritePermitProvider.getWritePermit(context, value, callingPid, callingUid, preference)
 }
 
 /** Provider of [KeyValueStore]. */
@@ -113,25 +94,3 @@
      */
     fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
 }
-
-/** Provider of read and write permit. */
-interface ReadWritePermitProvider {
-
-    val defaultReadWritePermit: @ReadWritePermit Int
-        get() = ReadWritePermit.DISALLOW
-
-    fun getReadPermit(
-        context: Context,
-        callingPid: Int,
-        callingUid: Int,
-        preference: PreferenceMetadata,
-    ): @ReadWritePermit Int = defaultReadWritePermit
-
-    fun getWritePermit(
-        context: Context,
-        value: Any?,
-        callingPid: Int,
-        callingUid: Int,
-        preference: PreferenceMetadata,
-    ): @ReadWritePermit Int = defaultReadWritePermit
-}
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index e173c5e..0f6a2a0 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -118,6 +118,7 @@
         spinner.setAdapter(mAdapter);
         spinner.setSelection(mPosition);
         spinner.setOnItemSelectedListener(mOnSelectedListener);
+        spinner.setLongClickable(false);
         if (mShouldPerformClick) {
             mShouldPerformClick = false;
             // To show dropdown view.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index 0c7d6f0..b173db0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -37,7 +37,7 @@
     private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
     private const val KEY_BT_BROADCAST_ID = "BI"
     private const val KEY_BT_BROADCAST_CODE = "BC"
-    private const val KEY_BT_STREAM_METADATA = "MD"
+    private const val KEY_BT_PUBLIC_METADATA = "PM"
     private const val KEY_BT_STANDARD_QUALITY = "SQ"
     private const val KEY_BT_HIGH_QUALITY = "HQ"
 
@@ -84,7 +84,7 @@
         }
         if (this.publicBroadcastMetadata != null &&
                 this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
-            entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
+            entries.add(Pair(KEY_BT_PUBLIC_METADATA, Base64.encodeToString(
                 this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
         }
         if ((this.audioConfigQuality and
@@ -160,7 +160,7 @@
         var sourceAdvertiserSid = -1
         var broadcastId = -1
         var broadcastName: String? = null
-        var streamMetadata: BluetoothLeAudioContentMetadata? = null
+        var publicMetadata: BluetoothLeAudioContentMetadata? = null
         var paSyncInterval = -1
         var broadcastCode: ByteArray? = null
         var audioConfigQualityStandard = -1
@@ -207,11 +207,11 @@
                     broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
                             .toByteArray(), Base64.NO_WRAP)
                 }
-                KEY_BT_STREAM_METADATA -> {
-                    require(streamMetadata == null) {
-                        "Duplicate streamMetadata $input"
+                KEY_BT_PUBLIC_METADATA -> {
+                    require(publicMetadata == null) {
+                        "Duplicate publicMetadata $input"
                     }
-                    streamMetadata = BluetoothLeAudioContentMetadata
+                    publicMetadata = BluetoothLeAudioContentMetadata
                         .fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
                 }
                 KEY_BT_STANDARD_QUALITY -> {
@@ -256,7 +256,7 @@
         Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
                 "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
                 "broadcastId=$broadcastId, broadcastName=$broadcastName, " +
-                "streamMetadata=${streamMetadata != null}, " +
+                "publicMetadata=${publicMetadata != null}, " +
                 "paSyncInterval=$paSyncInterval, " +
                 "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
                 "audioConfigQualityStandard=$audioConfigQualityStandard, " +
@@ -317,7 +317,7 @@
             setBroadcastName(broadcastName)
             // QR code should set PBP(public broadcast profile) for auracast
             setPublicBroadcast(true)
-            setPublicBroadcastMetadata(streamMetadata)
+            setPublicBroadcastMetadata(publicMetadata)
             setPaSyncInterval(paSyncInterval)
             setEncrypted(broadcastCode != null)
             setBroadcastCode(broadcastCode)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index bf86911..572444e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -30,11 +30,13 @@
 import androidx.annotation.ChecksSdkIntAtLeast;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -385,7 +387,7 @@
                 preferredMainDevice.refresh();
                 hasChanged = true;
             }
-            syncAudioSharingSourceIfNeeded(preferredMainDevice);
+            syncAudioSharingStatusIfNeeded(preferredMainDevice);
         }
         if (hasChanged) {
             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
@@ -399,13 +401,16 @@
         return userManager != null && userManager.isManagedProfile();
     }
 
-    private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) {
+    private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) {
         boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext);
-        if (isAudioSharingEnabled) {
+        if (isAudioSharingEnabled && mainDevice != null) {
             if (isWorkProfile()) {
-                log("addMemberDevicesIntoMainDevice: skip sync source for work profile");
+                log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile");
                 return;
             }
+            Set<CachedBluetoothDevice> deviceSet = new HashSet<>();
+            deviceSet.add(mainDevice);
+            deviceSet.addAll(mainDevice.getMemberDevice());
             boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager)
                     && BluetoothUtils.hasConnectedBroadcastSource(
                     mainDevice, mBtManager);
@@ -419,9 +424,6 @@
                 if (metadata != null && assistant != null) {
                     log("addMemberDevicesIntoMainDevice: sync audio sharing source after "
                             + "combining the top level devices.");
-                    Set<CachedBluetoothDevice> deviceSet = new HashSet<>();
-                    deviceSet.add(mainDevice);
-                    deviceSet.addAll(mainDevice.getMemberDevice());
                     Set<BluetoothDevice> sinksToSync = deviceSet.stream()
                             .map(CachedBluetoothDevice::getDevice)
                             .filter(device ->
@@ -435,8 +437,24 @@
                     }
                 }
             }
+            if (Flags.enableTemporaryBondDevicesUi()) {
+                log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing "
+                        + "sinks after combining the top level devices.");
+                Set<BluetoothDevice> sinksToSync = deviceSet.stream()
+                        .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect(
+                                Collectors.toSet());
+                if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) {
+                    for (BluetoothDevice device : sinksToSync) {
+                        if (!BluetoothUtils.isTemporaryBondDevice(device)) {
+                            log("addMemberDevicesIntoMainDevice: sync temp bond metadata for "
+                                    + device.getAnonymizedAddress());
+                            BluetoothUtils.setTemporaryBondMetadata(device);
+                        }
+                    }
+                }
+            }
         } else {
-            log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled");
+            log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled");
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
index 10156c4..bac564c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
@@ -20,6 +20,7 @@
 import android.content.pm.PackageManager
 import android.media.MediaMetadata
 import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
 import android.media.session.MediaSession
 import android.media.session.MediaSessionManager
 import android.media.session.PlaybackState
@@ -98,16 +99,22 @@
     }
 
     /** Set volume `level` to remote media `token` */
-    fun setVolume(token: MediaSession.Token, level: Int) {
+    fun setVolume(sessionId: SessionId, volumeLevel: Int) {
+        when (sessionId) {
+            is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel)
+        }
+    }
+
+    private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) {
         val record = mRecords[token]
         if (record == null) {
             Log.w(TAG, "setVolume: No record found for token $token")
             return
         }
         if (D.BUG) {
-            Log.d(TAG, "Setting level to $level")
+            Log.d(TAG, "Setting level to $volumeLevel")
         }
-        record.controller.setVolumeTo(level, 0)
+        record.controller.setVolumeTo(volumeLevel, 0)
     }
 
     private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) {
@@ -122,7 +129,7 @@
             )
         }
         val token = controller.sessionToken
-        mCallbacks.onRemoteVolumeChanged(token, flags)
+        mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags)
     }
 
     private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) {
@@ -158,7 +165,7 @@
                 controller.registerCallback(record, mHandler)
             }
             val record = mRecords[token]
-            val remote = isRemote(playbackInfo)
+            val remote = playbackInfo.isRemote()
             if (remote) {
                 updateRemoteH(token, record!!.name, playbackInfo)
                 record.sentRemote = true
@@ -172,7 +179,7 @@
                 Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote)
             }
             if (record.sentRemote) {
-                mCallbacks.onRemoteRemoved(token)
+                mCallbacks.onRemoteRemoved(SessionId.from(token))
                 record.sentRemote = false
             }
         }
@@ -213,8 +220,8 @@
     private fun updateRemoteH(
         token: MediaSession.Token,
         name: String?,
-        pi: MediaController.PlaybackInfo,
-    ) = mCallbacks.onRemoteUpdate(token, name, pi)
+        playbackInfo: PlaybackInfo,
+    ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo))
 
     private inner class MediaControllerRecord(val controller: MediaController) :
         MediaController.Callback() {
@@ -225,7 +232,7 @@
             return method + " " + controller.packageName + " "
         }
 
-        override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) {
+        override fun onAudioInfoChanged(info: PlaybackInfo) {
             if (D.BUG) {
                 Log.d(
                     TAG,
@@ -235,9 +242,9 @@
                         sentRemote),
                 )
             }
-            val remote = isRemote(info)
+            val remote = info.isRemote()
             if (!remote && sentRemote) {
-                mCallbacks.onRemoteRemoved(controller.sessionToken)
+                mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken))
                 sentRemote = false
             } else if (remote) {
                 updateRemoteH(controller.sessionToken, name, info)
@@ -301,20 +308,36 @@
         }
     }
 
+    /** Opaque id for ongoing sessions that support volume adjustment. */
+    sealed interface SessionId {
+
+        companion object {
+            fun from(token: MediaSession.Token) = Media(token)
+        }
+
+        data class Media(val token: MediaSession.Token) : SessionId
+    }
+
+    /** Holds session volume information. */
+    data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) {
+
+        companion object {
+
+            fun from(playbackInfo: PlaybackInfo) =
+                VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume)
+        }
+    }
+
     /** Callback for remote media sessions */
     interface Callbacks {
         /** Invoked when remote media session is updated */
-        fun onRemoteUpdate(
-            token: MediaSession.Token?,
-            name: String?,
-            pi: MediaController.PlaybackInfo?,
-        )
+        fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?)
 
         /** Invoked when remote media session is removed */
-        fun onRemoteRemoved(token: MediaSession.Token?)
+        fun onRemoteRemoved(token: SessionId?)
 
         /** Invoked when remote volume is changed */
-        fun onRemoteVolumeChanged(token: MediaSession.Token?, flags: Int)
+        fun onRemoteVolumeChanged(token: SessionId?, flags: Int)
     }
 
     companion object {
@@ -325,12 +348,11 @@
         const val UPDATE_REMOTE_SESSION_LIST: Int = 3
 
         private const val USE_SERVICE_LABEL = false
-
-        private fun isRemote(pi: MediaController.PlaybackInfo?): Boolean =
-            pi != null && pi.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE
     }
 }
 
+private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+
 private fun MediaController.dump(n: Int, writer: PrintWriter) {
     writer.println("  Controller $n: $packageName")
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index fd14d1f..2eccaa6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -40,6 +40,8 @@
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import com.android.settingslib.flags.Flags;
@@ -74,6 +76,9 @@
     private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
     private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
     private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33";
+    private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+    private static final String TEMP_BOND_METADATA =
+            "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>";
     private final static int GROUP1 = 1;
     private final BluetoothClass DEVICE_CLASS_1 =
             createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES);
@@ -337,6 +342,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
     public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() {
         // Condition: The preferredDevice is main and there is another main device in top list
         // Expected Result: return true and there is the preferredDevice in top list
@@ -346,7 +352,6 @@
         mCachedDevices.add(preferredDevice);
         mCachedDevices.add(mCachedDevice2);
         mCachedDevices.add(mCachedDevice3);
-        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 
         assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                 .isTrue();
@@ -359,6 +364,7 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
     public void
             addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() {
         // Condition: The preferredDevice is main and there is another main device in top list
@@ -369,7 +375,6 @@
         mCachedDevices.add(preferredDevice);
         mCachedDevices.add(mCachedDevice2);
         mCachedDevices.add(mCachedDevice3);
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         when(mBroadcast.isEnabled(null)).thenReturn(true);
         BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
         when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -377,6 +382,8 @@
                 BluetoothLeBroadcastReceiveState.class);
         when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
         when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+        when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+                .thenReturn(TEMP_BOND_METADATA.getBytes());
         when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
         when(mUserManager.isManagedProfile()).thenReturn(true);
 
@@ -387,10 +394,13 @@
         assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
         assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
         verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false);
+        verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
     }
 
     @Test
-    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() {
+    @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
+    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() {
         // Condition: The preferredDevice is main and there is another main device in top list
         // Expected Result: return true and there is the preferredDevice in top list
         CachedBluetoothDevice preferredDevice = mCachedDevice1;
@@ -399,7 +409,6 @@
         mCachedDevices.add(preferredDevice);
         mCachedDevices.add(mCachedDevice2);
         mCachedDevices.add(mCachedDevice3);
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         when(mBroadcast.isEnabled(null)).thenReturn(true);
         BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
         when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -407,6 +416,8 @@
                 BluetoothLeBroadcastReceiveState.class);
         when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
         when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+        when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+                .thenReturn(TEMP_BOND_METADATA.getBytes());
 
         assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                 .isTrue();
@@ -415,6 +426,8 @@
         assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
         assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
         verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false);
+        verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
     }
 
     @Test
@@ -436,13 +449,13 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
     public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() {
         // Condition: The preferredDevice is member and there are two main device in top list
         // Expected Result: return true and there is the preferredDevice in top list
         CachedBluetoothDevice preferredDevice = mCachedDevice2;
         BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
         mCachedDevice3.setGroupId(GROUP1);
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         when(mBroadcast.isEnabled(null)).thenReturn(false);
 
         assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
@@ -457,16 +470,20 @@
         assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
         verify(mAssistant, never()).addSource(any(BluetoothDevice.class),
                 any(BluetoothLeBroadcastMetadata.class), anyBoolean());
+        verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
+        verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
     }
 
     @Test
-    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() {
+    @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
+    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() {
         // Condition: The preferredDevice is member and there are two main device in top list
         // Expected Result: return true and there is the preferredDevice in top list
         CachedBluetoothDevice preferredDevice = mCachedDevice2;
         BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
         mCachedDevice3.setGroupId(GROUP1);
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         when(mBroadcast.isEnabled(null)).thenReturn(true);
         BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
         when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -474,6 +491,8 @@
                 BluetoothLeBroadcastReceiveState.class);
         when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
         when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state));
+        when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+                .thenReturn(TEMP_BOND_METADATA.getBytes());
 
         assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                 .isTrue();
@@ -488,6 +507,10 @@
         assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
         verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false);
         verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false);
+        verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
+        verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+                TEMP_BOND_METADATA.getBytes());
     }
 
     @Test
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
index 1ad20dc..5f6eb5e 100644
--- a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
@@ -233,7 +233,7 @@
 
         const val QR_CODE_STRING =
             "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" +
-            "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
+            "PM:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
         const val QR_CODE_STRING_NON_ENCRYPTED =
             "BLUETOOTH:UUID:184F;BN:SG9ja2V5;AT:0;AD:AABBCC001122;BI:DE51E9;SQ:1;AS:1;PI:FFFF;" +
             "NS:1;BS:1;NB:1;;"
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 246aa71..85617ba 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -809,7 +809,9 @@
                  Settings.Secure.DND_CONFIGS_MIGRATED,
                  Settings.Secure.NAVIGATION_MODE_RESTORE,
                  Settings.Secure.V_TO_U_RESTORE_ALLOWLIST,
-                 Settings.Secure.V_TO_U_RESTORE_DENYLIST);
+                 Settings.Secure.V_TO_U_RESTORE_DENYLIST,
+                 Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI,
+                 Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY);
 
     @Test
     public void systemSettingsBackedUpOrDenied() {
diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java
index 93690d4..b0fd925 100644
--- a/packages/Shell/src/com/android/shell/BugreportPrefs.java
+++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java
@@ -23,25 +23,24 @@
  * Preferences related to bug reports.
  */
 final class BugreportPrefs {
-    static final String PREFS_BUGREPORT = "bugreports";
-
-    private static final String KEY_WARNING_STATE = "warning-state";
-
-    static final int STATE_UNKNOWN = 0;
-    // Shows the warning dialog.
-    static final int STATE_SHOW = 1;
-    // Skips the warning dialog.
-    static final int STATE_HIDE = 2;
 
     static int getWarningState(Context context, int def) {
-        final SharedPreferences prefs = context.getSharedPreferences(
-                PREFS_BUGREPORT, Context.MODE_PRIVATE);
-        return prefs.getInt(KEY_WARNING_STATE, def);
+        String prefsBugreport = context.getResources().getString(
+                com.android.internal.R.string.prefs_bugreport);
+        String keyWarningState = context.getResources().getString(
+                com.android.internal.R.string.key_warning_state);
+        final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport,
+                Context.MODE_PRIVATE);
+        return prefs.getInt(keyWarningState, def);
     }
 
     static void setWarningState(Context context, int value) {
-        final SharedPreferences prefs = context.getSharedPreferences(
-                PREFS_BUGREPORT, Context.MODE_PRIVATE);
-        prefs.edit().putInt(KEY_WARNING_STATE, value).apply();
+        String prefsBugreport = context.getResources().getString(
+                com.android.internal.R.string.prefs_bugreport);
+        String keyWarningState = context.getResources().getString(
+                com.android.internal.R.string.key_warning_state);
+        final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport,
+                Context.MODE_PRIVATE);
+        prefs.edit().putInt(keyWarningState, value).apply();
     }
 }
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 61f49db..fb0678f 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -21,8 +21,6 @@
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
 import static com.android.shell.BugreportPrefs.getWarningState;
 import static com.android.shell.flags.Flags.handleBugreportsForWear;
 
@@ -1347,7 +1345,11 @@
     }
 
     private boolean hasUserDecidedNotToGetWarningMessage() {
-        return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE;
+        int bugreportStateUnknown = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_unknown);
+        int bugreportStateHide = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_hide);
+        return getWarningState(mContext, bugreportStateUnknown) == bugreportStateHide;
     }
 
     private void maybeShowWarningMessageAndCloseNotification(int id) {
diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
index a44e236..0e835f9 100644
--- a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
+++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
@@ -16,9 +16,6 @@
 
 package com.android.shell;
 
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_SHOW;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
 import static com.android.shell.BugreportPrefs.getWarningState;
 import static com.android.shell.BugreportPrefs.setWarningState;
 import static com.android.shell.BugreportProgressService.sendShareIntent;
@@ -69,12 +66,19 @@
 
         mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox);
 
-        final int state = getWarningState(this, STATE_UNKNOWN);
+        int bugreportStateUnknown = getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_unknown);
+        int bugreportStateHide = getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_hide);
+        int bugreportStateShow = getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_show);
+
+        final int state = getWarningState(this, bugreportStateUnknown);
         final boolean checked;
         if (Build.IS_USER) {
-            checked = state == STATE_HIDE; // Only checks if specifically set to.
+            checked = state == bugreportStateHide; // Only checks if specifically set to.
         } else {
-            checked = state != STATE_SHOW; // Checks by default.
+            checked = state != bugreportStateShow; // Checks by default.
         }
         mConfirmRepeat.setChecked(checked);
 
@@ -83,9 +87,14 @@
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
+        int bugreportStateHide = getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_hide);
+        int bugreportStateShow = getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_show);
         if (which == AlertDialog.BUTTON_POSITIVE) {
             // Remember confirm state, and launch target
-            setWarningState(this, mConfirmRepeat.isChecked() ? STATE_HIDE : STATE_SHOW);
+            setWarningState(this, mConfirmRepeat.isChecked() ? bugreportStateHide
+                    : bugreportStateShow);
             if (mSendIntent != null) {
                 sendShareIntent(this, mSendIntent);
             }
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 7bda2ea..2d6abe6 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -19,10 +19,6 @@
 import static android.test.MoreAsserts.assertContainsRegex;
 
 import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME;
-import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT;
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_SHOW;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
 import static com.android.shell.BugreportPrefs.getWarningState;
 import static com.android.shell.BugreportPrefs.setWarningState;
 import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED;
@@ -201,8 +197,9 @@
             return null;
         }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
                 any(), anyBoolean(), anyBoolean());
-
-        setWarningState(mContext, STATE_HIDE);
+        int bugreportStateHide = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_hide);
+        setWarningState(mContext, bugreportStateHide);
 
         mUiBot.turnScreenOn();
     }
@@ -469,22 +466,31 @@
 
     @Test
     public void testBugreportFinished_withWarningUnknownState() throws Exception {
-        bugreportFinishedWithWarningTest(STATE_UNKNOWN);
+        int bugreportStateUnknown = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_unknown);
+        bugreportFinishedWithWarningTest(bugreportStateUnknown);
     }
 
     @Test
     public void testBugreportFinished_withWarningShowAgain() throws Exception {
-        bugreportFinishedWithWarningTest(STATE_SHOW);
+        int bugreportStateShow = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_show);
+        bugreportFinishedWithWarningTest(bugreportStateShow);
     }
 
     private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception {
+        int bugreportStateUnknown = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_unknown);
+        int bugreportStateHide = mContext.getResources().getInteger(
+                com.android.internal.R.integer.bugreport_state_hide);
         if (propertyState == null) {
             // Clear properties
-            mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE)
-                    .edit().clear().commit();
+            mContext.getSharedPreferences(
+                    mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport)
+                            , Context.MODE_PRIVATE).edit().clear().commit();
             // Confidence check...
-            assertEquals("Did not reset properties", STATE_UNKNOWN,
-                    getWarningState(mContext, STATE_UNKNOWN));
+            assertEquals("Did not reset properties", bugreportStateUnknown,
+                    getWarningState(mContext, bugreportStateUnknown));
         } else {
             setWarningState(mContext, propertyState);
         }
@@ -501,7 +507,8 @@
         // TODO: get ok and dontShowAgain from the dialog reference above
         UiObject dontShowAgain =
                 mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat));
-        final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN;
+        final boolean firstTime =
+                propertyState == null || propertyState == bugreportStateUnknown;
         if (firstTime) {
             if (Build.IS_USER) {
                 assertFalse("Checkbox should NOT be checked by default on user builds",
@@ -524,8 +531,8 @@
         assertActionSendMultiple(extras);
 
         // Make sure it's hidden now.
-        int newState = getWarningState(mContext, STATE_UNKNOWN);
-        assertEquals("Didn't change state", STATE_HIDE, newState);
+        int newState = getWarningState(mContext, bugreportStateUnknown);
+        assertEquals("Didn't change state", bugreportStateHide, newState);
     }
 
     @Test
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 744388f..19806e7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -207,6 +207,8 @@
         "tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java",
         "tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt",
         "tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt",
+        "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java",
+        "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java",
         "tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java",
         "tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java",
         "tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java",
@@ -538,6 +540,7 @@
     kotlincflags: [
         "-Xjvm-default=all",
         "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+        "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
     ],
 
     plugins: [
@@ -552,6 +555,11 @@
     },
 }
 
+platform_compat_config {
+    name: "SystemUI-core-compat-config",
+    src: ":SystemUI-core",
+}
+
 filegroup {
     name: "AAA-src",
     srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"],
@@ -754,6 +762,7 @@
         "kosmos",
         "testables",
         "androidx.test.rules",
+        "platform-compat-test-rules",
     ],
     libs: [
         "android.test.runner.stubs.system",
@@ -888,6 +897,7 @@
     static_libs: [
         "RoboTestLibraries",
         "androidx.compose.runtime_runtime",
+        "platform-compat-test-rules",
     ],
     libs: [
         "android.test.runner.impl",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5b989cb..910f712 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1869,20 +1869,6 @@
     bug: "385194612"
 }
 
-flag{
-    name: "gsf_bouncer"
-    namespace: "systemui"
-    description: "Applies GSF font styles to Bouncer surfaces."
-    bug: "379364381"
-}
-
-flag {
-    name: "gsf_quick_settings"
-    namespace: "systemui"
-    description: "Applies GSF font styles to Quick Settings surfaces."
-    bug: "379364381"
-}
-
 flag {
     name: "spatial_model_launcher_pushback"
     namespace: "systemui"
@@ -1960,6 +1946,16 @@
 }
 
 flag {
+    name: "unfold_latency_tracking_fix"
+    namespace: "systemui"
+    description: "New implementation to track unfold latency that excludes broken cases"
+    bug: "390649568"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
     name: "ui_rich_ongoing_force_expanded"
     namespace: "systemui"
     description: "Force promoted notifications to always be expanded"
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index c63c2b4..9c6bb2c 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -42,6 +42,9 @@
         "//frameworks/libs/systemui:tracinglib-platform",
     ],
 
-    kotlincflags: ["-Xjvm-default=all"],
+    kotlincflags: [
+        "-Xjvm-default=all",
+        "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+    ],
     use_resource_processor: true,
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index fad8ae7..2f38dc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.UserAction
@@ -102,6 +103,8 @@
             viewModel,
             dialogFactory,
             Modifier.element(Bouncer.Elements.Content)
+                // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+                .testTag(Bouncer.Elements.Content.testTag)
                 .overscroll(verticalOverscrollEffect)
                 .sysuiResTag(Bouncer.TestTags.Root)
                 .fillMaxSize(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 4a4607b..0b17a3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -200,19 +200,15 @@
         scene(
             CommunalScenes.Blank,
             userActions =
-                if (viewModel.v2FlagEnabled()) emptyMap()
-                else mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal),
+                if (viewModel.swipeToHubEnabled())
+                    mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal)
+                else emptyMap(),
         ) {
             // This scene shows nothing only allowing for transitions to the communal scene.
             Box(modifier = Modifier.fillMaxSize())
         }
 
-        scene(
-            CommunalScenes.Communal,
-            userActions =
-                if (viewModel.v2FlagEnabled()) emptyMap()
-                else mapOf(Swipe.End to CommunalScenes.Blank),
-        ) {
+        scene(CommunalScenes.Communal, userActions = mapOf(Swipe.End to CommunalScenes.Blank)) {
             CommunalScene(
                 backgroundType = backgroundType,
                 colors = colors,
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 3c0480d..418a7a5 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
@@ -1705,15 +1705,38 @@
     contentScope: ContentScope?,
     modifier: Modifier = Modifier,
 ) {
-    if (SceneContainerFlag.isEnabled && contentScope != null) {
-        contentScope.MediaCarousel(
-            modifier = modifier.fillMaxSize(),
-            isVisible = true,
-            mediaHost = viewModel.mediaHost,
-            carouselController = viewModel.mediaCarouselController,
-        )
-    } else {
-        UmoLegacy(viewModel, modifier)
+    val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next)
+    val showPreviousActionLabel =
+        stringResource(R.string.accessibility_action_label_umo_show_previous)
+
+    Box(
+        modifier =
+            modifier.thenIf(!viewModel.isEditMode) {
+                Modifier.semantics {
+                    customActions =
+                        listOf(
+                            CustomAccessibilityAction(showNextActionLabel) {
+                                viewModel.onShowNextMedia()
+                                true
+                            },
+                            CustomAccessibilityAction(showPreviousActionLabel) {
+                                viewModel.onShowPreviousMedia()
+                                true
+                            },
+                        )
+                }
+            }
+    ) {
+        if (SceneContainerFlag.isEnabled && contentScope != null) {
+            contentScope.MediaCarousel(
+                modifier = modifier.fillMaxSize(),
+                isVisible = true,
+                mediaHost = viewModel.mediaHost,
+                carouselController = viewModel.mediaCarouselController,
+            )
+        } else {
+            UmoLegacy(viewModel, modifier)
+        }
     }
 }
 
@@ -1724,7 +1747,7 @@
             modifier
                 .clip(
                     shape =
-                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                        RoundedCornerShape(dimensionResource(R.dimen.notification_corner_radius))
                 )
                 .background(MaterialTheme.colorScheme.primary)
                 .pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
index 62aa31b..73a2425 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
@@ -50,7 +50,6 @@
 import androidx.window.layout.WindowMetricsCalculator
 import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_HEIGHT
 import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_WIDTH
-import com.android.systemui.communal.util.WindowSizeUtils.MEDIUM_WIDTH
 
 /**
  * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain
@@ -267,9 +266,8 @@
 }
 
 private fun calculateNumCellsWidth(width: Dp) =
-    // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
     when {
-        width >= MEDIUM_WIDTH -> 3
+        width >= 900.dp -> 3
         width >= COMPACT_WIDTH -> 2
         else -> 1
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 5e61af6..aa07370 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
@@ -55,7 +56,11 @@
 
     @Composable
     override fun ContentScope.Content(modifier: Modifier) {
-        LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier)
+        LockscreenScene(
+            lockscreenContent = lockscreenContent,
+            // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+            modifier = modifier.testTag(key.rootElementKey.testTag),
+        )
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
index ba25719..0abed39 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
@@ -26,18 +26,16 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalDensity
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.plugins.clocks.ClockController
 import kotlin.math.min
 import kotlin.math.roundToInt
 
 /** Produces a [BurnInState] that can be used to query the `LockscreenBurnInViewModel` flows. */
 @Composable
-fun rememberBurnIn(
-    clockInteractor: KeyguardClockInteractor,
-): BurnInState {
-    val clock by clockInteractor.currentClock.collectAsStateWithLifecycle()
+fun rememberBurnIn(clockViewModel: KeyguardClockViewModel): BurnInState {
+    val clock by clockViewModel.currentClock.collectAsStateWithLifecycle()
 
     val (smartspaceTop, onSmartspaceTopChanged) = remember { mutableStateOf<Float?>(null) }
     val (smallClockTop, onSmallClockTopChanged) = remember { mutableStateOf<Float?>(null) }
@@ -62,18 +60,12 @@
 }
 
 @Composable
-private fun rememberBurnInParameters(
-    clock: ClockController?,
-    topmostTop: Int,
-): BurnInParameters {
+private fun rememberBurnInParameters(clock: ClockController?, topmostTop: Int): BurnInParameters {
     val density = LocalDensity.current
     val topInset = WindowInsets.systemBars.union(WindowInsets.displayCutout).getTop(density)
 
     return remember(clock, topInset, topmostTop) {
-        BurnInParameters(
-            topInset = topInset,
-            minViewY = topmostTop,
-        )
+        BurnInParameters(topInset = topInset, minViewY = topmostTop)
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index abf7fdc..f51049a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -38,11 +38,11 @@
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
 import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
@@ -89,7 +89,7 @@
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val aodPromotedNotificationViewModelFactory: AODPromotedNotificationViewModel.Factory,
     private val systemBarUtilsState: SystemBarUtilsState,
-    private val clockInteractor: KeyguardClockInteractor,
+    private val keyguardClockViewModel: KeyguardClockViewModel,
 ) {
 
     init {
@@ -118,7 +118,7 @@
 
         val isVisible by
             keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle()
-        val burnIn = rememberBurnIn(clockInteractor)
+        val burnIn = rememberBurnIn(keyguardClockViewModel)
 
         AnimatedVisibility(
             visible = isVisible,
@@ -141,7 +141,7 @@
                 isVisible.stopAnimating()
             }
         }
-        val burnIn = rememberBurnIn(clockInteractor)
+        val burnIn = rememberBurnIn(keyguardClockViewModel)
         AnimatedVisibility(
             visibleState = transitionState,
             enter = fadeIn(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 410499a..6293fc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -37,7 +37,6 @@
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
 import com.android.compose.modifiers.thenIf
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene
@@ -56,7 +55,7 @@
     private val mediaCarouselSection: MediaCarouselSection,
     private val clockSection: DefaultClockSection,
     private val weatherClockSection: WeatherClockSection,
-    private val clockInteractor: KeyguardClockInteractor,
+    private val keyguardClockViewModel: KeyguardClockViewModel,
 ) {
     @Composable
     fun ContentScope.DefaultClockLayout(
@@ -138,7 +137,7 @@
         smartSpacePaddingTop: (Resources) -> Int,
         modifier: Modifier = Modifier,
     ) {
-        val burnIn = rememberBurnIn(clockInteractor)
+        val burnIn = rememberBurnIn(keyguardClockViewModel)
 
         Column(modifier = modifier) {
             with(clockSection) {
@@ -163,7 +162,7 @@
         smartSpacePaddingTop: (Resources) -> Int,
         shouldOffSetClockToOneHalf: Boolean = false,
     ) {
-        val burnIn = rememberBurnIn(clockInteractor)
+        val burnIn = rememberBurnIn(keyguardClockViewModel)
         val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
 
         LaunchedEffect(isLargeClockVisible) {
@@ -204,7 +203,7 @@
         smartSpacePaddingTop: (Resources) -> Int,
         modifier: Modifier = Modifier,
     ) {
-        val burnIn = rememberBurnIn(clockInteractor)
+        val burnIn = rememberBurnIn(keyguardClockViewModel)
         val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
         val currentClockState = clockViewModel.currentClock.collectAsStateWithLifecycle()
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index d7d4e17..09b8d17 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -175,7 +175,7 @@
     viewModel: NotificationsPlaceholderViewModel,
 ) {
 
-    val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
+    val isSnoozable by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
 
     var scrollOffset by remember { mutableFloatStateOf(0f) }
     val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
@@ -192,7 +192,7 @@
         )
     }
 
-    val nestedScrollConnection =
+    val snoozeScrollConnection =
         object : NestedScrollConnection {
             override suspend fun onPreFling(available: Velocity): Velocity {
                 if (
@@ -206,7 +206,7 @@
             }
         }
 
-    LaunchedEffect(isHeadsUp) { scrollOffset = 0f }
+    LaunchedEffect(isSnoozable) { scrollOffset = 0f }
 
     LaunchedEffect(scrollableState.isScrollInProgress) {
         if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) {
@@ -230,10 +230,8 @@
                             ),
                     )
                 }
-                .thenIf(isHeadsUp) {
-                    Modifier.nestedScroll(nestedScrollConnection)
-                        .scrollable(orientation = Orientation.Vertical, state = scrollableState)
-                },
+                .thenIf(isSnoozable) { Modifier.nestedScroll(snoozeScrollConnection) }
+                .scrollable(orientation = Orientation.Vertical, state = scrollableState),
     )
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 7c50d6f..64f3cb1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -30,9 +30,9 @@
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
 import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
@@ -57,7 +57,7 @@
     private val shadeSession: SaveableSession,
     private val stackScrollView: Lazy<NotificationScrollView>,
     private val clockSection: DefaultClockSection,
-    private val clockInteractor: KeyguardClockInteractor,
+    private val keyguardClockViewModel: KeyguardClockViewModel,
 ) : Overlay {
     override val key = Overlays.NotificationsShade
 
@@ -86,7 +86,7 @@
 
         OverlayShade(
             panelElement = NotificationsShade.Elements.Panel,
-            panelAlignment = Alignment.TopStart,
+            alignmentOnWideScreens = Alignment.TopStart,
             modifier = modifier,
             onScrimClicked = viewModel::onScrimClicked,
             header = {
@@ -105,7 +105,7 @@
             Box {
                 Column {
                     if (viewModel.showClock) {
-                        val burnIn = rememberBurnIn(clockInteractor)
+                        val burnIn = rememberBurnIn(keyguardClockViewModel)
 
                         with(clockSection) {
                             SmallClock(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index cc58b8e..afdb3cb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -128,7 +128,7 @@
             )
             OverlayShade(
                 panelElement = QuickSettingsShade.Elements.Panel,
-                panelAlignment = Alignment.TopEnd,
+                alignmentOnWideScreens = Alignment.TopEnd,
                 onScrimClicked = contentViewModel::onScrimClicked,
                 header = {
                     OverlayShadeHeader(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index da4e582..aa0d474 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -46,6 +46,8 @@
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
 import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
+import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
 import com.android.systemui.lifecycle.rememberActivated
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.qs.ui.composable.QuickSettingsTheme
@@ -202,7 +204,7 @@
         SceneTransitionLayout(
             state = state,
             modifier = modifier.fillMaxSize(),
-            swipeSourceDetector = viewModel.edgeDetector,
+            swipeSourceDetector = viewModel.swipeSourceDetector,
         ) {
             sceneByKey.forEach { (sceneKey, scene) ->
                 scene(
@@ -239,7 +241,12 @@
         BottomRightCornerRibbon(
             content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
             colorSaturation = { viewModel.ribbonColorSaturation },
-            modifier = Modifier.align(Alignment.BottomEnd),
+            modifier =
+                Modifier.align(Alignment.BottomEnd)
+                    .burnInAware(
+                        viewModel = viewModel.burnIn,
+                        params = rememberBurnIn(viewModel.clock).parameters,
+                    ),
         )
     }
 }
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 5dcec5b..cdb1e2e 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
@@ -59,7 +59,7 @@
 @Composable
 fun ContentScope.OverlayShade(
     panelElement: ElementKey,
-    panelAlignment: Alignment,
+    alignmentOnWideScreens: Alignment,
     onScrimClicked: () -> Unit,
     modifier: Modifier = Modifier,
     header: @Composable () -> Unit,
@@ -71,7 +71,7 @@
 
         Box(
             modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth),
-            contentAlignment = panelAlignment,
+            contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens,
         ) {
             Panel(
                 modifier =
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 090e9cc..42dd85a3 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -45,6 +45,9 @@
         "mechanics",
     ],
 
-    kotlincflags: ["-Xjvm-default=all"],
+    kotlincflags: [
+        "-Xjvm-default=all",
+        "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+    ],
     use_resource_processor: true,
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 907b5bc..05958a2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -169,7 +169,7 @@
             Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
         }
         .then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
-        .testTag(key.testTag)
+        .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
 }
 
 /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 72bb82b..d47210c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -65,6 +65,8 @@
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
+    // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting().
+    implicitTestTags: Boolean = false,
     builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
 ) {
     SceneTransitionLayoutForTesting(
@@ -73,6 +75,7 @@
         swipeSourceDetector,
         swipeDetector,
         transitionInterceptionThreshold,
+        implicitTestTags = implicitTestTags,
         onLayoutImpl = null,
         builder = builder,
     )
@@ -725,10 +728,8 @@
 }
 
 /**
- * An internal version of [SceneTransitionLayout] to be used for tests.
- *
- * Important: You should use this only in tests and if you need to access the underlying
- * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the
+ * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements.
  */
 @Composable
 internal fun SceneTransitionLayoutForTesting(
@@ -741,6 +742,7 @@
     sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
     ancestors: List<Ancestor> = remember { emptyList() },
     lookaheadScope: LookaheadScope? = null,
+    implicitTestTags: Boolean = true,
     builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -765,6 +767,7 @@
                 directionChangeSlop = directionChangeSlop,
                 defaultEffectFactory = defaultEffectFactory,
                 decayAnimationSpec = decayAnimationSpec,
+                implicitTestTags = implicitTestTags,
             )
             .also { onLayoutImpl?.invoke(it) }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 53996d2..e3c4eb0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -122,6 +122,9 @@
      * This is used to enable transformations and shared elements across NestedSTLs.
      */
     internal val ancestors: List<Ancestor> = emptyList(),
+
+    /** Whether elements and scene should be tagged using `Modifier.testTag`. */
+    internal val implicitTestTags: Boolean = false,
     lookaheadScope: LookaheadScope? = null,
     defaultEffectFactory: OverscrollFactory,
 ) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 95d6440..64cfe38 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -171,7 +171,7 @@
                 .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
                     Modifier.container(containerState)
                 }
-                .testTag(key.testTag)
+                .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
         ) {
             CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
                 scope.content()
@@ -290,6 +290,7 @@
             sharedElementMap = layoutImpl.elements,
             ancestors = ancestors,
             lookaheadScope = layoutImpl.lookaheadScope,
+            implicitTestTags = layoutImpl.implicitTestTags,
         )
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index f625add..fa10f66 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -227,7 +227,7 @@
             to = SceneB,
             transitionLayout = { state ->
                 coroutineScope = rememberCoroutineScope()
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) {
                         Box(Modifier.size(layoutSize)) {
                             // Transformed element
@@ -633,7 +633,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
                     scene(SceneB) {}
                 }
@@ -674,7 +674,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(Modifier.fillMaxSize())
                     }
@@ -734,7 +734,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(
                             Modifier.overscroll(verticalOverscrollEffect)
@@ -834,7 +834,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(Modifier.fillMaxSize())
                     }
@@ -893,7 +893,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(
+                SceneTransitionLayoutForTesting(
                     state = state,
                     modifier = Modifier.size(layoutWidth, layoutHeight),
                 ) {
@@ -970,7 +970,7 @@
 
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            SceneTransitionLayout(
+            SceneTransitionLayoutForTesting(
                 state = state,
                 modifier = Modifier.size(layoutWidth, layoutHeight),
             ) {
@@ -1057,7 +1057,7 @@
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
 
-            SceneTransitionLayout(state) {
+            SceneTransitionLayoutForTesting(state) {
                 scene(SceneA) {
                     Box(Modifier.size(layoutSize)) {
                         Box(
@@ -1374,7 +1374,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                     scene(SceneA) {
                         Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
                     }
@@ -1742,7 +1742,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state, Modifier.size(200.dp)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                     scene(SceneA) { Foo(offset = 0.dp) }
                     scene(SceneB) { Foo(offset = 20.dp) }
                     scene(SceneC) { Foo(offset = 40.dp) }
@@ -1828,7 +1828,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
 
                     // Define A after B so that Foo is placed in A during A <=> B.
@@ -1887,7 +1887,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) { Foo() }
                     scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
                 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index 04c762f..98ecb64 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -90,7 +90,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA) { Foo() }
             }
@@ -132,7 +132,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA) { Foo() }
                 overlay(OverlayB) { Foo() }
@@ -230,7 +230,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
                 overlay(OverlayA) { MovableBar() }
                 overlay(OverlayB) { MovableBar() }
@@ -302,7 +302,7 @@
             }
         var alignment by mutableStateOf(Alignment.Center)
         rule.setContent {
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA, alignment = alignment) { Foo() }
             }
@@ -761,7 +761,7 @@
         val movableElementChildTag = "movableElementChildTag"
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) {
                         MovableElement(key, Modifier) {
                             content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 2bf2358..366b11d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -250,7 +250,7 @@
             }
 
         rule.setContent {
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) }
                 overlay(OverlayA) { Box(Modifier.fillMaxSize()) }
                 overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 3c490ae..c877d99 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -97,7 +97,7 @@
             MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions)
         }
 
-        SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
+        SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) {
             scene(SceneA, userActions = mapOf(Back to SceneB)) {
                 Box(Modifier.fillMaxSize()) {
                     SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 751b314..11abbbe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -763,7 +763,7 @@
         var touchSlop = 0f
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                 scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
                     Box(Modifier.fillMaxSize())
                 }
@@ -837,7 +837,7 @@
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
             CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                     scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
                         Box(Modifier.fillMaxSize())
                     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
index bb511bc..8b56892 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -40,7 +40,7 @@
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests
 import com.android.compose.animation.scene.Scale
 import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
 import com.android.compose.animation.scene.SceneTransitions
 import com.android.compose.animation.scene.TestScenes
 import com.android.compose.animation.scene.testNestedTransition
@@ -114,7 +114,7 @@
         @Composable
         (states: List<MutableSceneTransitionLayoutState>) -> Unit =
         { states ->
-            SceneTransitionLayout(states[0]) {
+            SceneTransitionLayoutForTesting(states[0]) {
                 scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
                 scene(
                     TestScenes.SceneB,
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 6d47bab..e56d1be 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -30,5 +30,7 @@
     content: @Composable ContentScope.() -> Unit,
 ) {
     val state = rememberMutableSceneTransitionLayoutState(currentScene)
-    SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
+    SceneTransitionLayout(state, modifier, implicitTestTags = true) {
+        scene(currentScene, content = content)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index f94a7ed..a362a37 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -137,7 +137,7 @@
         },
         changeState = changeState,
         transitionLayout = { state ->
-            SceneTransitionLayout(state, layoutModifier) {
+            SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
             }
@@ -163,7 +163,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(fromScene) { fromSceneContent() }
                 overlay(overlay) { overlayContent() }
             }
@@ -191,7 +191,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(toScene) { toSceneContent() }
                 overlay(overlay) { overlayContent() }
             }
@@ -223,7 +223,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(currentScene) { currentSceneContent() }
                 overlay(from, alignment = fromAlignment) { fromContent() }
                 overlay(to, alignment = toAlignment) { toContent() }
@@ -273,7 +273,7 @@
                 }
             }
 
-            SceneTransitionLayout(state, layoutModifier) {
+            SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
             }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 2e5b5b5..aad1276 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -113,8 +113,8 @@
 
     companion object {
         // 750ms @ 120hz -> 90 frames of animation
-        // In practice, 45 looks good enough
-        const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45
+        // In practice, 30 looks good enough and limits our memory usage
+        const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
 
         val FLEX_TYPEFACE by lazy {
             // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index fe665e6..24b9e84 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -84,6 +84,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
 import com.google.common.truth.Truth
 import junit.framework.Assert
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -280,9 +281,9 @@
                 kosmos.keyguardDismissTransitionInteractor,
                 { primaryBouncerInteractor },
                 executor,
-            ) {
-                deviceEntryInteractor
-            }
+                { deviceEntryInteractor },
+                { kosmos.windowRootViewBlurInteractor },
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index bd33e52..f53f964 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -64,12 +64,12 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import java.util.List;
-import java.util.Optional;
-
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
 import platform.test.runner.parameterized.Parameters;
 
+import java.util.List;
+import java.util.Optional;
+
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -171,6 +171,7 @@
                 mActivityStarter,
                 mKeyguardInteractor,
                 mSceneInteractor,
+                mKosmos.getShadeRepository(),
                 Optional.of(() -> mWindowRootView));
 
         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 494e0b4..dd43d81 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -74,12 +74,12 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import java.util.List;
-import java.util.Optional;
-
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
 import platform.test.runner.parameterized.Parameters;
 
+import java.util.List;
+import java.util.Optional;
+
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -187,6 +187,7 @@
                 mActivityStarter,
                 mKeyguardInteractor,
                 mSceneInteractor,
+                mKosmos.getShadeRepository(),
                 Optional.of(() -> mWindowRootView)
         );
 
@@ -627,6 +628,22 @@
         onRemovedCallbackCaptor.getValue().onRemoved();
     }
 
+    @Test
+    public void testTouchSessionStart_notifiesShadeOfUserInteraction() {
+        mTouchHandler.onSessionStart(mTouchSession);
+
+        mKosmos.getTestScope().getTestScheduler().runCurrent();
+        assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isTrue();
+
+        ArgumentCaptor<TouchHandler.TouchSession.Callback> onRemovedCallbackCaptor =
+                ArgumentCaptor.forClass(TouchHandler.TouchSession.Callback.class);
+        verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture());
+        onRemovedCallbackCaptor.getValue().onRemoved();
+
+        mKosmos.getTestScope().getTestScheduler().runCurrent();
+        assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isFalse();
+    }
+
     private void swipeToPosition(float percent, float velocityY) {
         Mockito.clearInvocations(mTouchSession);
         mTouchHandler.onSessionStart(mTouchSession);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt
new file mode 100644
index 0000000..0c97750
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.wakefulnessLifecycle
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceInactiveConditionTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().useUnconfinedTestDispatcher().also {
+            whenever(it.wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_AWAKE
+        }
+
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            DeviceInactiveCondition(
+                applicationCoroutineScope,
+                keyguardStateController,
+                wakefulnessLifecycle,
+                keyguardUpdateMonitor,
+                keyguardInteractor,
+                JavaAdapter(applicationCoroutineScope),
+            )
+        }
+
+    @Test
+    fun asleep_conditionTrue() =
+        kosmos.runTest {
+            // Condition is false to start.
+            underTest.start()
+            assertThat(underTest.isConditionMet).isFalse()
+
+            // Condition is true when device goes to sleep.
+            sleep()
+            assertThat(underTest.isConditionMet).isTrue()
+        }
+
+    @Test
+    fun dozingAndAsleep_conditionFalse() =
+        kosmos.runTest {
+            // Condition is true when device is asleep.
+            underTest.start()
+            sleep()
+            assertThat(underTest.isConditionMet).isTrue()
+
+            // Condition turns false after doze starts.
+            fakeKeyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE)
+            )
+            assertThat(underTest.isConditionMet).isFalse()
+        }
+
+    fun Kosmos.sleep() {
+        whenever(wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_ASLEEP
+        argumentCaptor<WakefulnessLifecycle.Observer>().apply {
+            verify(wakefulnessLifecycle).addObserver(capture())
+            firstValue.onStartedGoingToSleep()
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 1a3606e..da25bca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -44,6 +45,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -65,7 +67,7 @@
 
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
     private val underTest: CommunalTransitionViewModel by lazy {
         kosmos.communalTransitionViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 18cc8bf..522650b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -23,24 +23,19 @@
 import android.content.pm.UserInfo
 import android.provider.Settings
 import android.view.accessibility.AccessibilityEvent
-import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.accessibilityManager
 import android.widget.RemoteViews
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.uiEventLogger
+import com.android.internal.logging.uiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
-import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
-import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
-import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
@@ -49,12 +44,15 @@
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.controller.mediaCarouselController
 import com.android.systemui.media.controls.ui.view.MediaHost
@@ -62,73 +60,45 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceTimeBy
-import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalEditModeViewModelTest : SysuiTestCase() {
-    @Mock private lateinit var mediaHost: MediaHost
-    @Mock private lateinit var uiEventLogger: UiEventLogger
-    @Mock private lateinit var packageManager: PackageManager
-    @Mock private lateinit var metricsLogger: CommunalMetricsLogger
-
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-
-    private lateinit var tutorialRepository: FakeCommunalTutorialRepository
-    private lateinit var widgetRepository: FakeCommunalWidgetRepository
-    private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository
-    private lateinit var mediaRepository: FakeCommunalMediaRepository
-    private lateinit var communalSceneInteractor: CommunalSceneInteractor
-    private lateinit var communalInteractor: CommunalInteractor
-    private lateinit var accessibilityManager: AccessibilityManager
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
 
     private val testableResources = context.orCreateTestableResources
 
-    private lateinit var underTest: CommunalEditModeViewModel
+    private val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() }
 
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
+    private val Kosmos.metricsLogger by Kosmos.Fixture { mock<CommunalMetricsLogger>() }
 
-        tutorialRepository = kosmos.fakeCommunalTutorialRepository
-        widgetRepository = kosmos.fakeCommunalWidgetRepository
-        smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
-        mediaRepository = kosmos.fakeCommunalMediaRepository
-        communalSceneInteractor = kosmos.communalSceneInteractor
-        communalInteractor = spy(kosmos.communalInteractor)
-        kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
-        kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
-        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
-        accessibilityManager = kosmos.accessibilityManager
-
-        underTest =
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
             CommunalEditModeViewModel(
                 communalSceneInteractor,
                 communalInteractor,
-                kosmos.communalSettingsInteractor,
-                kosmos.keyguardTransitionInteractor,
-                mediaHost,
+                communalSettingsInteractor,
+                keyguardTransitionInteractor,
+                mock<MediaHost>(),
                 uiEventLogger,
                 logcatLogBuffer("CommunalEditModeViewModelTest"),
-                kosmos.testDispatcher,
+                testDispatcher,
                 metricsLogger,
                 context,
                 accessibilityManager,
@@ -136,19 +106,28 @@
                 WIDGET_PICKER_PACKAGE_NAME,
                 kosmos.mediaCarouselController,
             )
+        }
+
+    @Before
+    fun setUp() {
+        kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
+        kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
+        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
     }
 
     @Test
     fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() =
-        testScope.runTest {
-            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+        kosmos.runTest {
+            fakeCommunalTutorialRepository.setTutorialSettingState(
+                Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
+            )
 
             // Widgets available.
-            widgetRepository.addWidget(appWidgetId = 0, rank = 30)
-            widgetRepository.addWidget(appWidgetId = 1, rank = 20)
+            fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30)
+            fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20)
 
             // Smartspace available.
-            smartspaceRepository.setTimers(
+            fakeCommunalSmartspaceRepository.setTimers(
                 listOf(
                     CommunalSmartspaceTimer(
                         smartspaceTargetId = "target",
@@ -159,7 +138,7 @@
             )
 
             // Media playing.
-            mediaRepository.mediaActive()
+            fakeCommunalMediaRepository.mediaActive()
 
             val communalContent by collectLastValue(underTest.communalContent)
 
@@ -173,7 +152,7 @@
 
     @Test
     fun selectedKey_onReorderWidgets_isSet() =
-        testScope.runTest {
+        kosmos.runTest {
             val selectedKey by collectLastValue(underTest.selectedKey)
 
             underTest.setSelectedKey(null)
@@ -186,7 +165,7 @@
 
     @Test
     fun isCommunalContentVisible_isTrue_whenEditModeShowing() =
-        testScope.runTest {
+        kosmos.runTest {
             val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible)
             communalSceneInteractor.setEditModeState(EditModeState.SHOWING)
             assertThat(isCommunalContentVisible).isEqualTo(true)
@@ -194,7 +173,7 @@
 
     @Test
     fun isCommunalContentVisible_isFalse_whenEditModeNotShowing() =
-        testScope.runTest {
+        kosmos.runTest {
             val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible)
             communalSceneInteractor.setEditModeState(null)
             assertThat(isCommunalContentVisible).isEqualTo(false)
@@ -202,12 +181,14 @@
 
     @Test
     fun deleteWidget() =
-        testScope.runTest {
-            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+        kosmos.runTest {
+            fakeCommunalTutorialRepository.setTutorialSettingState(
+                Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
+            )
 
             // Widgets available.
-            widgetRepository.addWidget(appWidgetId = 0, rank = 30)
-            widgetRepository.addWidget(appWidgetId = 1, rank = 20)
+            fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30)
+            fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20)
 
             val communalContent by collectLastValue(underTest.communalContent)
 
@@ -233,26 +214,38 @@
         }
 
     @Test
-    fun reorderWidget_uiEventLogging_start() {
-        underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123))
-        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
-    }
+    fun reorderWidget_uiEventLogging_start() =
+        kosmos.runTest {
+            underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123))
+
+            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+            assertThat(uiEventLoggerFake.logs[0].eventId)
+                .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START.id)
+        }
 
     @Test
-    fun reorderWidget_uiEventLogging_end() {
-        underTest.onReorderWidgetEnd()
-        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
-    }
+    fun reorderWidget_uiEventLogging_end() =
+        kosmos.runTest {
+            underTest.onReorderWidgetEnd()
+
+            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+            assertThat(uiEventLoggerFake.logs[0].eventId)
+                .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH.id)
+        }
 
     @Test
-    fun reorderWidget_uiEventLogging_cancel() {
-        underTest.onReorderWidgetCancel()
-        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
-    }
+    fun reorderWidget_uiEventLogging_cancel() =
+        kosmos.runTest {
+            underTest.onReorderWidgetCancel()
+
+            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+            assertThat(uiEventLoggerFake.logs[0].eventId)
+                .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL.id)
+        }
 
     @Test
     fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
-        testScope.runTest {
+        kosmos.runTest {
             var activityStarted = false
             val success =
                 underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
@@ -266,7 +259,7 @@
 
     @Test
     fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
-        testScope.runTest {
+        kosmos.runTest {
             val success =
                 underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
                     run { throw ActivityNotFoundException() }
@@ -278,7 +271,7 @@
 
     @Test
     fun showDisclaimer_trueAfterEditModeShowing() =
-        testScope.runTest {
+        kosmos.runTest {
             val showDisclaimer by collectLastValue(underTest.showDisclaimer)
 
             assertThat(showDisclaimer).isFalse()
@@ -288,9 +281,9 @@
 
     @Test
     fun showDisclaimer_falseWhenDismissed() =
-        testScope.runTest {
+        kosmos.runTest {
             underTest.setEditModeState(EditModeState.SHOWING)
-            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+            fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
 
             val showDisclaimer by collectLastValue(underTest.showDisclaimer)
 
@@ -301,63 +294,67 @@
 
     @Test
     fun showDisclaimer_trueWhenTimeout() =
-        testScope.runTest {
+        kosmos.runTest {
             underTest.setEditModeState(EditModeState.SHOWING)
-            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+            fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
 
             val showDisclaimer by collectLastValue(underTest.showDisclaimer)
 
             assertThat(showDisclaimer).isTrue()
             underTest.onDisclaimerDismissed()
             assertThat(showDisclaimer).isFalse()
-            advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS)
+            testScope.advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS + 1.milliseconds)
             assertThat(showDisclaimer).isTrue()
         }
 
     @Test
-    fun scrollPosition_persistedOnEditCleanup() {
-        val index = 2
-        val offset = 30
-        underTest.onScrollPositionUpdated(index, offset)
-        underTest.cleanupEditModeState()
+    fun scrollPosition_persistedOnEditCleanup() =
+        kosmos.runTest {
+            val index = 2
+            val offset = 30
+            underTest.onScrollPositionUpdated(index, offset)
+            underTest.cleanupEditModeState()
 
-        verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
-    }
+            assertThat(communalInteractor.firstVisibleItemIndex).isEqualTo(index)
+            assertThat(communalInteractor.firstVisibleItemOffset).isEqualTo(offset)
+        }
 
     @Test
-    fun onNewWidgetAdded_accessibilityDisabled_doNothing() {
-        whenever(accessibilityManager.isEnabled).thenReturn(false)
+    fun onNewWidgetAdded_accessibilityDisabled_doNothing() =
+        kosmos.runTest {
+            whenever(accessibilityManager.isEnabled).thenReturn(false)
 
-        val provider =
-            mock<AppWidgetProviderInfo> {
-                on { loadLabel(packageManager) }.thenReturn("Test Clock")
-            }
-        underTest.onNewWidgetAdded(provider)
+            val provider =
+                mock<AppWidgetProviderInfo> {
+                    on { loadLabel(packageManager) }.thenReturn("Test Clock")
+                }
+            underTest.onNewWidgetAdded(provider)
 
-        verify(accessibilityManager, never()).sendAccessibilityEvent(any())
-    }
+            verify(accessibilityManager, never()).sendAccessibilityEvent(any())
+        }
 
     @Test
-    fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() {
-        whenever(accessibilityManager.isEnabled).thenReturn(true)
+    fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() =
+        kosmos.runTest {
+            whenever(accessibilityManager.isEnabled).thenReturn(true)
 
-        val provider =
-            mock<AppWidgetProviderInfo> {
-                on { loadLabel(packageManager) }.thenReturn("Test Clock")
-            }
-        underTest.onNewWidgetAdded(provider)
+            val provider =
+                mock<AppWidgetProviderInfo> {
+                    on { loadLabel(packageManager) }.thenReturn("Test Clock")
+                }
+            underTest.onNewWidgetAdded(provider)
 
-        val captor = argumentCaptor<AccessibilityEvent>()
-        verify(accessibilityManager).sendAccessibilityEvent(captor.capture())
+            val captor = argumentCaptor<AccessibilityEvent>()
+            verify(accessibilityManager).sendAccessibilityEvent(captor.capture())
 
-        val event = captor.firstValue
-        assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT)
-        assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen")
-    }
+            val event = captor.firstValue
+            assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+            assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen")
+        }
 
     @Test
     fun onResizeWidget_logsMetrics() =
-        testScope.runTest {
+        kosmos.runTest {
             val appWidgetId = 123
             val spanY = 2
             val widgetIdToRankMap = mapOf(appWidgetId to 1)
@@ -372,7 +369,6 @@
                 rank = rank,
             )
 
-            verify(communalInteractor).resizeWidget(appWidgetId, spanY, widgetIdToRankMap)
             verify(metricsLogger)
                 .logResizeWidget(
                     componentName = componentName.flattenToString(),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8515515..799054a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -78,6 +78,7 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.controller.mediaCarouselController
+import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -120,6 +121,7 @@
 @RunWith(ParameterizedAndroidJunit4::class)
 class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
     @Mock private lateinit var metricsLogger: CommunalMetricsLogger
 
     private val kosmos = testKosmos()
@@ -161,6 +163,8 @@
 
         kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
         whenever(mediaHost.visible).thenReturn(true)
+        whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler)
+            .thenReturn(mediaCarouselScrollHandler)
 
         kosmos.powerInteractor.setAwakeForTest()
 
@@ -187,6 +191,7 @@
             metricsLogger,
             kosmos.mediaCarouselController,
             kosmos.blurConfig,
+            false,
         )
     }
 
@@ -903,6 +908,20 @@
         }
 
     @Test
+    fun onShowPreviousMedia_scrollHandler_isCalled() =
+        testScope.runTest {
+            underTest.onShowPreviousMedia()
+            verify(mediaCarouselScrollHandler).scrollByStep(-1)
+        }
+
+    @Test
+    fun onShowNextMedia_scrollHandler_isCalled() =
+        testScope.runTest {
+            underTest.onShowNextMedia()
+            verify(mediaCarouselScrollHandler).scrollByStep(1)
+        }
+
+    @Test
     @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
     fun uiIsBlurred_whenPrimaryBouncerIsShowing() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 329627a..e36d245 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.eq
 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.TestScope
@@ -73,6 +74,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
@@ -80,21 +82,26 @@
     private val testScope: TestScope = kosmos.testScope
 
     private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor
+
     private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
     private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
     private val fakeUserRepository = kosmos.fakeUserRepository
     private val facePropertyRepository = kosmos.facePropertyRepository
-    private val fakeDeviceEntryFingerprintAuthInteractor =
-        kosmos.deviceEntryFingerprintAuthInteractor
-    private val powerInteractor = kosmos.powerInteractor
     private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
 
-    private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
+    private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor }
     private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
     private val trustManager = kosmos.trustManager
-    private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor
+
+    private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
+    private val fakeDeviceEntryFingerprintAuthInteractor by lazy {
+        kosmos.deviceEntryFingerprintAuthInteractor
+    }
+    private val powerInteractor by lazy { kosmos.powerInteractor }
+    private val deviceEntryFaceAuthStatusInteractor by lazy {
+        kosmos.deviceEntryFaceAuthStatusInteractor
+    }
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
index e0082da..ff5fa39 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.Notification
 import android.app.NotificationManager
+import android.service.notification.StatusBarNotification
 import androidx.annotation.StringRes
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -28,6 +29,7 @@
 import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
 import com.android.systemui.kosmos.backgroundScope
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.settings.userTracker
 import com.android.systemui.statusbar.commandline.commandRegistry
@@ -35,6 +37,7 @@
 import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.hours
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -44,23 +47,29 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.times
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.firstValue
 import org.mockito.kotlin.never
+import org.mockito.kotlin.secondValue
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class TutorialNotificationCoordinatorTest : SysuiTestCase() {
 
     private lateinit var underTest: TutorialNotificationCoordinator
-    private val kosmos = testKosmos()
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val keyboardRepository = FakeKeyboardRepository()
     private val touchpadRepository = FakeTouchpadRepository()
     private lateinit var repository: TutorialSchedulerRepository
     @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var notification: StatusBarNotification
     @Captor private lateinit var notificationCaptor: ArgumentCaptor<Notification>
     @get:Rule val rule = MockitoJUnit.rule()
 
@@ -107,6 +116,7 @@
     fun showTouchpadNotification() = runTestAndClear {
         touchpadRepository.setIsAnyTouchpadConnected(true)
         testScope.advanceTimeBy(LAUNCH_DELAY)
+        mockExistingNotification()
         verifyNotification(
             R.string.launch_touchpad_tutorial_notification_title,
             R.string.launch_touchpad_tutorial_notification_content,
@@ -131,6 +141,45 @@
             .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), any(), any())
     }
 
+    @Test
+    fun showKeyboardNotificationThenDisconnectKeyboard() = runTestAndClear {
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+        verifyNotification(
+            R.string.launch_keyboard_tutorial_notification_title,
+            R.string.launch_keyboard_tutorial_notification_content,
+        )
+        mockExistingNotification()
+
+        // After the keyboard is disconnected, i.e. there is nothing connected, the notification
+        // should be cancelled
+        keyboardRepository.setIsAnyKeyboardConnected(false)
+        verify(notificationManager).cancelAsUser(eq(TAG), eq(NOTIFICATION_ID), any())
+    }
+
+    @Test
+    fun showKeyboardTouchpadNotificationThenDisconnectKeyboard() = runTestAndClear {
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+        touchpadRepository.setIsAnyTouchpadConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+        mockExistingNotification()
+        keyboardRepository.setIsAnyKeyboardConnected(false)
+
+        verify(notificationManager, times(2))
+            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
+        // Connect both device and the first notification is for both
+        notificationCaptor.firstValue.verify(
+            R.string.launch_keyboard_touchpad_tutorial_notification_title,
+            R.string.launch_keyboard_touchpad_tutorial_notification_content,
+        )
+        // After the keyboard is disconnected, i.e. with only the touchpad left, the notification
+        // should be update to the one for only touchpad
+        notificationCaptor.secondValue.verify(
+            R.string.launch_touchpad_tutorial_notification_title,
+            R.string.launch_touchpad_tutorial_notification_content,
+        )
+    }
+
     private fun runTestAndClear(block: suspend () -> Unit) =
         testScope.runTest {
             try {
@@ -140,12 +189,21 @@
             }
         }
 
+    // Assume that there's an existing notification when the updater checks activeNotifications
+    private fun mockExistingNotification() {
+        whenever(notification.id).thenReturn(NOTIFICATION_ID)
+        whenever(notificationManager.activeNotifications).thenReturn(arrayOf(notification))
+    }
+
     private fun verifyNotification(@StringRes titleResId: Int, @StringRes contentResId: Int) {
         verify(notificationManager)
             .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
-        val notification = notificationCaptor.value
-        val actualTitle = notification.getString(Notification.EXTRA_TITLE)
-        val actualContent = notification.getString(Notification.EXTRA_TEXT)
+        notificationCaptor.value.verify(titleResId, contentResId)
+    }
+
+    private fun Notification.verify(@StringRes titleResId: Int, @StringRes contentResId: Int) {
+        val actualTitle = getString(Notification.EXTRA_TITLE)
+        val actualContent = getString(Notification.EXTRA_TEXT)
         assertThat(actualTitle).isEqualTo(context.getString(titleResId))
         assertThat(actualContent).isEqualTo(context.getString(contentResId))
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index e13f3f1..7e93f5a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -20,19 +20,26 @@
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
 import android.service.dream.dreamManager
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
+import com.android.systemui.Flags.glanceableHubV2
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
 import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy
 import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
@@ -56,11 +63,14 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth
 import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -93,7 +103,10 @@
         @JvmStatic
         @Parameters(name = "{0}")
         fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+            return FlagsParameterization.allCombinationsOf(
+                FLAG_COMMUNAL_SCENE_KTF_REFACTOR,
+                FLAG_GLANCEABLE_HUB_V2,
+            )
         }
     }
 
@@ -107,6 +120,7 @@
 
         // Transition to DOZING and set the power interactor asleep.
         kosmos.powerInteractor.setAsleepForTest()
+        kosmos.setCommunalV2ConfigEnabled(true)
         runBlocking {
             kosmos.transitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
@@ -160,7 +174,7 @@
 
     @Test
     @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
-    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2)
     fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() =
         kosmos.runTest {
             whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
@@ -179,7 +193,17 @@
     fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() =
         kosmos.runTest {
             setCommunalAvailable(true)
-            whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            if (glanceableHubV2()) {
+                val user = fakeUserRepository.asMainUser()
+                fakeSettings.putIntForUser(
+                    Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                    1,
+                    user.id,
+                )
+                batteryRepository.fake.setDevicePluggedIn(true)
+            } else {
+                whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            }
 
             clearInvocations(fakeCommunalSceneRepository)
             powerInteractor.setAwakeForTest()
@@ -240,7 +264,17 @@
     fun testTransitionToGlanceableHub_onWakeup_ifAvailable() =
         kosmos.runTest {
             setCommunalAvailable(true)
-            whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            if (glanceableHubV2()) {
+                val user = fakeUserRepository.asMainUser()
+                fakeSettings.putIntForUser(
+                    Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                    1,
+                    user.id,
+                )
+                batteryRepository.fake.setDevicePluggedIn(true)
+            } else {
+                whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            }
 
             // Device turns on.
             powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index 8e10682..5882cff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -19,14 +19,20 @@
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
 import android.service.dream.dreamManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.glanceableHubV2
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
 import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -46,6 +52,8 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.advanceTimeBy
@@ -66,7 +74,10 @@
         @JvmStatic
         @Parameters(name = "{0}")
         fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+            return FlagsParameterization.allCombinationsOf(
+                    FLAG_COMMUNAL_SCENE_KTF_REFACTOR,
+                    FLAG_GLANCEABLE_HUB_V2,
+                )
                 .andSceneContainer()
         }
     }
@@ -101,6 +112,7 @@
             )
             reset(kosmos.transitionRepository)
             kosmos.setCommunalAvailable(true)
+            kosmos.setCommunalV2ConfigEnabled(true)
         }
         kosmos.underTest.start()
     }
@@ -202,7 +214,17 @@
             reset(transitionRepository)
 
             setCommunalAvailable(true)
-            whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            if (glanceableHubV2()) {
+                val user = fakeUserRepository.asMainUser()
+                fakeSettings.putIntForUser(
+                    Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                    1,
+                    user.id,
+                )
+                batteryRepository.fake.setDevicePluggedIn(true)
+            } else {
+                whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            }
 
             // Device wakes up.
             powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 29e95cd..fee2dfc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -53,13 +54,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
     val kosmos = testKosmos()
-    val underTest = kosmos.keyguardTransitionInteractor
-    val repository = kosmos.fakeKeyguardTransitionRepository
     val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+
+    val underTest by lazy { kosmos.keyguardTransitionInteractor }
 
     @Test
     fun transitionCollectorsReceivesOnlyAppropriateEvents() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index a0fed6b..0ec3173 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -57,9 +57,7 @@
                     duration = 1000.milliseconds,
                     edge = Edge.create(from = Scenes.Gone, to = DREAMING),
                 )
-                .setupWithoutSceneContainer(
-                    edge = Edge.create(from = GONE, to = DREAMING),
-                )
+                .setupWithoutSceneContainer(edge = Edge.create(from = GONE, to = DREAMING))
     }
 
     @Test(expected = IllegalArgumentException::class)
@@ -75,7 +73,7 @@
                 underTest.sharedFlow(
                     startTime = 300.milliseconds,
                     duration = 800.milliseconds,
-                    onStep = { it }
+                    onStep = { it },
                 )
         }
 
@@ -112,6 +110,17 @@
         }
 
     @Test
+    fun onStepReturnsNullEmitsNothing() =
+        testScope.runTest {
+            val flow = underTest.sharedFlow(duration = 100.milliseconds, onStep = { null })
+            var animationValues = collectLastValue(flow)
+            runCurrent()
+
+            repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+            assertThat(animationValues()).isNull()
+        }
+
+    @Test
     fun usesStartTime() =
         testScope.runTest {
             val flow =
@@ -166,11 +175,7 @@
     @Test
     fun usesOnStepToDoubleValue() =
         testScope.runTest {
-            val flow =
-                underTest.sharedFlow(
-                    duration = 1000.milliseconds,
-                    onStep = { it * 2 },
-                )
+            val flow = underTest.sharedFlow(duration = 1000.milliseconds, onStep = { it * 2 })
             val animationValues by collectLastValue(flow)
             runCurrent()
 
@@ -190,10 +195,7 @@
     fun usesOnStepToDoubleValueWithState() =
         testScope.runTest {
             val flow =
-                underTest.sharedFlowWithState(
-                    duration = 1000.milliseconds,
-                    onStep = { it * 2 },
-                )
+                underTest.sharedFlowWithState(duration = 1000.milliseconds, onStep = { it * 2 })
             val animationValues by collectLastValue(flow)
             runCurrent()
 
@@ -204,7 +206,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.STARTED,
-                        value = 0f
+                        value = 0f,
                     )
                 )
             repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
@@ -214,7 +216,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.RUNNING,
-                        value = 0.6f
+                        value = 0.6f,
                     )
                 )
             repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
@@ -224,7 +226,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.RUNNING,
-                        value = 1.2f
+                        value = 1.2f,
                     )
                 )
             repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
@@ -234,7 +236,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.RUNNING,
-                        value = 1.6f
+                        value = 1.6f,
                     )
                 )
             repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
@@ -244,7 +246,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.RUNNING,
-                        value = 2f
+                        value = 2f,
                     )
                 )
             repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
@@ -254,7 +256,7 @@
                         from = GONE,
                         to = DREAMING,
                         transitionState = TransitionState.FINISHED,
-                        value = null
+                        value = null,
                     )
                 )
         }
@@ -262,11 +264,7 @@
     @Test
     fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() =
         testScope.runTest {
-            val flow =
-                underTest.sharedFlow(
-                    duration = 1000.milliseconds,
-                    onStep = { it },
-                )
+            val flow = underTest.sharedFlow(duration = 1000.milliseconds, onStep = { it })
             val values by collectValues(flow)
             runCurrent()
 
@@ -280,11 +278,7 @@
     @Test
     fun sameFloatValueWithADifferentTransitionStateDoesEmitTwice() =
         testScope.runTest {
-            val flow =
-                underTest.sharedFlow(
-                    duration = 1000.milliseconds,
-                    onStep = { it },
-                )
+            val flow = underTest.sharedFlow(duration = 1000.milliseconds, onStep = { it })
             val values by collectValues(flow)
             runCurrent()
 
@@ -302,14 +296,14 @@
 
     private fun step(
         value: Float,
-        state: TransitionState = TransitionState.RUNNING
+        state: TransitionState = TransitionState.RUNNING,
     ): TransitionStep {
         return TransitionStep(
             from = GONE,
             to = DREAMING,
             value = value,
             transitionState = state,
-            ownerName = "GoneToDreamingTransitionViewModelTest"
+            ownerName = "GoneToDreamingTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelTest.kt
index c8fade3..6648ae2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelTest.kt
@@ -24,6 +24,7 @@
 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.TransitionState.FINISHED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -63,7 +64,15 @@
             )
             runCurrent()
 
-            assertThat(alpha).isEqualTo(0.0f)
+            assertThat(alpha).isEqualTo(1.0f)
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                listOf(lockscreenToDozing(1f, FINISHED)),
+                testScope,
+            )
+            runCurrent()
+
+            assertThat(alpha).isEqualTo(0f)
         }
 
     private fun lockscreenToDozing(value: Float, state: TransitionState = RUNNING): TransitionStep {
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 b66e2fe..f357d0c 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
@@ -41,7 +41,7 @@
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.disableDualShade
 import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -69,7 +69,7 @@
         @Parameters(
             name =
                 "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," +
-                    " isSingleShade={3}, isShadeTouchable={4}, isOccluded={6}"
+                    " isSingleShade={3}, isShadeTouchable={4}, isOccluded={5}"
         )
         @JvmStatic
         fun combinations() = buildList {
@@ -275,20 +275,20 @@
                 assertThat(downDestination?.transitionKey).isNull()
             }
 
-            val downFromTopRightDestination =
+            val downFromEndHalfDestination =
                 userActions?.get(
                     Swipe.Down(
-                        fromSource = SceneContainerEdge.TopRight,
+                        fromSource = SceneContainerArea.EndHalf,
                         pointerCount = if (downWithTwoPointers) 2 else 1,
                     )
                 )
             when {
-                !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull()
-                downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull()
+                !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull()
+                downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull()
                 else -> {
-                    assertThat(downFromTopRightDestination)
+                    assertThat(downFromEndHalfDestination)
                         .isEqualTo(ShowOverlay(Overlays.QuickSettingsShade))
-                    assertThat(downFromTopRightDestination?.transitionKey).isNull()
+                    assertThat(downFromEndHalfDestination?.transitionKey).isNull()
                 }
             }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
index d073cf1..c2f0ab9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
@@ -16,8 +16,11 @@
 
 package com.android.systemui.media.controls.ui.view
 
+import android.content.res.Resources
 import android.testing.TestableLooper
 import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -25,16 +28,21 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.Mock
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -42,7 +50,9 @@
 class MediaCarouselScrollHandlerTest : SysuiTestCase() {
 
     private val carouselWidth = 1038
+    private val settingsButtonWidth = 200
     private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0)
+    private lateinit var testableLooper: TestableLooper
 
     @Mock lateinit var mediaCarousel: MediaScrollView
     @Mock lateinit var pageIndicator: PageIndicator
@@ -53,6 +63,9 @@
     @Mock lateinit var falsingManager: FalsingManager
     @Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit
     @Mock lateinit var logger: MediaUiEventLogger
+    @Mock lateinit var contentContainer: ViewGroup
+    @Mock lateinit var settingsButton: View
+    @Mock lateinit var resources: Resources
 
     lateinit var executor: FakeExecutor
     private val clock = FakeSystemClock()
@@ -63,6 +76,11 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         executor = FakeExecutor(clock)
+        testableLooper = TestableLooper.get(this)
+        PhysicsAnimatorTestUtils.prepareForTest()
+        PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
+
+        whenever(mediaCarousel.contentContainer).thenReturn(contentContainer)
         mediaCarouselScrollHandler =
             MediaCarouselScrollHandler(
                 mediaCarousel,
@@ -74,13 +92,17 @@
                 closeGuts,
                 falsingManager,
                 logSmartspaceImpression,
-                logger
+                logger,
             )
         mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth
-
         whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener)
     }
 
+    @After
+    fun tearDown() {
+        PhysicsAnimatorTestUtils.tearDown()
+    }
+
     @Test
     fun testCarouselScroll_shortScroll() {
         whenever(mediaCarousel.isLayoutRtl).thenReturn(false)
@@ -128,4 +150,109 @@
 
         verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
     }
+
+    @Test
+    fun testCarouselScrollByStep_scrollRight() {
+        setupMediaContainer(visibleIndex = 0)
+
+        mediaCarouselScrollHandler.scrollByStep(1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt())
+    }
+
+    @Test
+    fun testCarouselScrollByStep_scrollLeft() {
+        setupMediaContainer(visibleIndex = 1)
+
+        mediaCarouselScrollHandler.scrollByStep(-1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
+    }
+
+    @Test
+    fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() {
+        setupMediaContainer(visibleIndex = 1)
+
+        mediaCarouselScrollHandler.scrollByStep(1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
+        verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
+    }
+
+    @Test
+    fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() {
+        setupMediaContainer(visibleIndex = 0)
+
+        mediaCarouselScrollHandler.scrollByStep(-1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
+        verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
+    }
+
+    @Test
+    fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() {
+        setupMediaContainer(visibleIndex = 0)
+        PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
+        whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
+
+        mediaCarouselScrollHandler.scrollByStep(-1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
+        verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
+    }
+
+    @Test
+    fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() {
+        setupMediaContainer(visibleIndex = 1)
+        PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
+        whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
+
+        mediaCarouselScrollHandler.scrollByStep(1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
+        verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
+    }
+
+    @Test
+    fun testScrollByStep_noScroll_notDismissible() {
+        setupMediaContainer(visibleIndex = 1, showsSettingsButton = false)
+
+        mediaCarouselScrollHandler.scrollByStep(1)
+        clock.advanceTime(DISMISS_DELAY)
+        executor.runAllReady()
+
+        verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
+        verify(mediaCarousel, never()).animationTargetX = anyFloat()
+    }
+
+    private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) {
+        whenever(contentContainer.childCount).thenReturn(2)
+        val child1: View = mock()
+        val child2: View = mock()
+        whenever(child1.left).thenReturn(0)
+        whenever(child2.left).thenReturn(carouselWidth)
+        whenever(contentContainer.getChildAt(0)).thenReturn(child1)
+        whenever(contentContainer.getChildAt(1)).thenReturn(child2)
+
+        whenever(settingsButton.width).thenReturn(settingsButtonWidth)
+        whenever(settingsButton.context).thenReturn(context)
+        whenever(settingsButton.resources).thenReturn(resources)
+        whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20)
+        mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton)
+
+        mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex
+        mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
index 52b9e47..52a0a54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
 import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -71,13 +71,13 @@
         }
 
     @Test
-    fun downFromTopRight_switchesToQuickSettingsShade() =
+    fun downFromTopEnd_switchesToQuickSettingsShade() =
         testScope.runTest {
             val actions by collectLastValue(underTest.actions)
             underTest.activateIn(this)
 
             val action =
-                (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay)
+                (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay)
             assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade)
             val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
             assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
index 264eda5..668c606 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.settings.userFileManager
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
@@ -76,11 +77,11 @@
     @Test
     fun setLargeTilesSpecs_inSharedPreferences() {
         val setA = setOf("tileA", "tileB")
-        underTest.setLargeTilesSpecs(setA.toTileSpecs())
+        underTest.writeLargeTileSpecs(setA.toTileSpecs())
         assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA)
 
         val setB = setOf("tileA", "tileB")
-        underTest.setLargeTilesSpecs(setB.toTileSpecs())
+        underTest.writeLargeTileSpecs(setB.toTileSpecs())
         assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB)
     }
 
@@ -92,12 +93,12 @@
 
                 fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
                 val setA = setOf("tileA", "tileB")
-                underTest.setLargeTilesSpecs(setA.toTileSpecs())
+                underTest.writeLargeTileSpecs(setA.toTileSpecs())
                 assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA)
 
                 fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
                 val setB = setOf("tileA", "tileB")
-                underTest.setLargeTilesSpecs(setB.toTileSpecs())
+                underTest.writeLargeTileSpecs(setB.toTileSpecs())
                 assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB)
 
                 fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
@@ -106,7 +107,7 @@
         }
 
     @Test
-    fun setInitialTilesFromSettings_noLargeTiles_tilesSet() =
+    fun setUpgradePathFromSettings_noLargeTiles_tilesSet() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -117,14 +118,17 @@
 
                 assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse()
 
-                underTest.setInitialLargeTilesSpecs(tiles, PRIMARY_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(tiles),
+                    PRIMARY_USER_ID,
+                )
 
                 assertThat(largeTiles).isEqualTo(tiles)
             }
         }
 
     @Test
-    fun setInitialTilesFromSettings_alreadyLargeTiles_tilesNotSet() =
+    fun setUpgradePathFromSettings_alreadyLargeTiles_tilesNotSet() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -133,14 +137,17 @@
                 fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
                 setLargeTilesSpecsInSharedPreferences(setOf("tileC"))
 
-                underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+                    ANOTHER_USER_ID,
+                )
 
                 assertThat(largeTiles).isEqualTo(setOf("tileC").toTileSpecs())
             }
         }
 
     @Test
-    fun setInitialTilesFromSettings_emptyLargeTiles_tilesNotSet() =
+    fun setUpgradePathFromSettings_emptyLargeTiles_tilesNotSet() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -149,14 +156,17 @@
                 fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
                 setLargeTilesSpecsInSharedPreferences(emptySet())
 
-                underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+                    ANOTHER_USER_ID,
+                )
 
                 assertThat(largeTiles).isEmpty()
             }
         }
 
     @Test
-    fun setInitialTilesFromSettings_nonCurrentUser_tilesSetForCorrectUser() =
+    fun setUpgradePathFromSettings_nonCurrentUser_tilesSetForCorrectUser() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -164,7 +174,10 @@
                 fakeUserRepository.setUserInfos(USERS)
                 fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
 
-                underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+                    ANOTHER_USER_ID,
+                )
 
                 assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
 
@@ -174,7 +187,7 @@
         }
 
     @Test
-    fun setInitialTiles_afterDefaultRead_noSetOnRepository_initialTilesCorrect() =
+    fun setUpgradePath_afterDefaultRead_noSetOnRepository_initialTilesCorrect() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -186,14 +199,17 @@
                 assertThat(currentLargeTiles).isNotEmpty()
 
                 val tiles = setOf("tileA", "tileB")
-                underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()),
+                    PRIMARY_USER_ID,
+                )
 
                 assertThat(largeTiles).isEqualTo(tiles.toTileSpecs())
             }
         }
 
     @Test
-    fun setInitialTiles_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() =
+    fun setUpgradePath_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() =
         with(kosmos) {
             testScope.runTest {
                 val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -204,15 +220,80 @@
 
                 assertThat(currentLargeTiles).isNotEmpty()
 
-                underTest.setLargeTilesSpecs(setOf(TileSpec.create("tileC")))
+                underTest.writeLargeTileSpecs(setOf(TileSpec.create("tileC")))
 
                 val tiles = setOf("tileA", "tileB")
-                underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID)
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()),
+                    PRIMARY_USER_ID,
+                )
 
                 assertThat(largeTiles).isEqualTo(setOf(TileSpec.create("tileC")))
             }
         }
 
+    @Test
+    fun setTilesRestored_noLargeTiles_tilesSet() =
+        with(kosmos) {
+            testScope.runTest {
+                val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+                fakeUserRepository.setUserInfos(USERS)
+                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+                val tiles = setOf("tileA", "tileB").toTileSpecs()
+
+                assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse()
+
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.RestoreFromBackup(tiles),
+                    PRIMARY_USER_ID,
+                )
+
+                assertThat(largeTiles).isEqualTo(tiles)
+            }
+        }
+
+    @Test
+    fun setDefaultTilesInitial_defaultSetLarge() =
+        with(kosmos) {
+            testScope.runTest {
+                val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+                fakeUserRepository.setUserInfos(USERS)
+                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.DefaultSet,
+                    PRIMARY_USER_ID,
+                )
+
+                assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+            }
+        }
+
+    @Test
+    fun setTilesRestored_afterDefaultSet_tilesSet() =
+        with(kosmos) {
+            testScope.runTest {
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.DefaultSet,
+                    PRIMARY_USER_ID,
+                )
+                val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+                fakeUserRepository.setUserInfos(USERS)
+                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+                val tiles = setOf("tileA", "tileB").toTileSpecs()
+
+                underTest.setInitialOrUpgradeLargeTiles(
+                    TilesUpgradePath.RestoreFromBackup(tiles),
+                    PRIMARY_USER_ID,
+                )
+
+                assertThat(largeTiles).isEqualTo(tiles)
+            }
+        }
+
     private fun getSharedPreferences(): SharedPreferences =
         with(kosmos) {
             return userFileManager.getSharedPreferences(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt
new file mode 100644
index 0000000..f3c1f0c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.common.shared.model.PackageChangeModel.Empty.packageName
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository
+import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor
+import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository
+import com.android.systemui.qs.pipeline.data.repository.defaultTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
+import com.android.systemui.settings.userFileManager
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.userRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LargeTilesUpgradePathsTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply { defaultTilesRepository = DefaultTilesQSHostRepository(mainResources) }
+
+    private val defaultTiles = kosmos.defaultTilesRepository.defaultTiles.toSet()
+
+    private val underTest = kosmos.qsPreferencesInteractor
+
+    private val Kosmos.userId
+        get() = userRepository.getSelectedUserInfo().id
+
+    private val Kosmos.intent
+        get() =
+            Intent(ACTION_RESTORE_FINISHED).apply {
+                `package` = packageName
+                putExtra(Intent.EXTRA_USER_ID, kosmos.userId)
+                flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
+            }
+
+    /**
+     * This test corresponds to the case of a fresh start.
+     *
+     * The resulting large tiles are the default set of large tiles.
+     */
+    @Test
+    fun defaultTiles_noDataInSharedPreferences_defaultLargeTiles() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+            assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+        }
+
+    /**
+     * This test corresponds to a user that upgraded in place from a build that didn't support large
+     * tiles to one that does. The current tiles of the user are read from settings.
+     *
+     * The resulting large tiles are those that were read from Settings.
+     */
+    @Test
+    fun upgradeInPlace_noDataInSharedPreferences_allLargeTiles() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val tiles = setOf("a", "b", "c").toTileSpecs()
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(tiles),
+                userId,
+            )
+
+            assertThat(largeTiles).isEqualTo(tiles)
+        }
+
+    /**
+     * This test corresponds to a fresh start, and then the user restarts the device, without ever
+     * having modified the set of large tiles.
+     *
+     * The resulting large tiles are the default large tiles that were set on the fresh start
+     */
+    @Test
+    fun defaultSet_restartDevice_largeTilesDontChange() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+            // User restarts the device, this will send a read from settings with the default
+            // set of tiles
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(defaultTiles),
+                userId,
+            )
+
+            assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+        }
+
+    /**
+     * This test corresponds to a fresh start, following the user changing the sizes of some tiles.
+     * After that, the user restarts the device.
+     *
+     * The resulting set of large tiles are those that the user determined before restarting the
+     * device.
+     */
+    @Test
+    fun defaultSet_someSizeChanges_restart_correctSet() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+            underTest.setLargeTilesSpecs(largeTiles!! + setOf("a", "b").toTileSpecs())
+            val largeTilesBeforeRestart = largeTiles!!
+
+            // Restart
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(defaultTiles),
+                userId,
+            )
+            assertThat(largeTiles).isEqualTo(largeTilesBeforeRestart)
+        }
+
+    /**
+     * This test corresponds to a user that upgraded, and after that performed some size changes.
+     * After that, the user restarts the device.
+     *
+     * The resulting set of large tiles are those that the user determined before restarting the
+     * device.
+     */
+    @Test
+    fun readFromSettings_changeSizes_restart_newLargeSet() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val readTiles = setOf("a", "b", "c").toTileSpecs()
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(readTiles),
+                userId,
+            )
+            underTest.setLargeTilesSpecs(emptySet())
+
+            assertThat(largeTiles).isEmpty()
+
+            // Restart
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(readTiles),
+                userId,
+            )
+            assertThat(largeTiles).isEmpty()
+        }
+
+    /**
+     * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+     * one that does, via restore from backup. Note that there's no file in SharedPreferences to
+     * restore.
+     *
+     * The resulting set of large tiles are those that were restored from the backup.
+     */
+    @Test
+    fun restoreFromBackup_noDataInSharedPreferences_allLargeTiles() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val tiles = setOf("a", "b", "c").toTileSpecs()
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.RestoreFromBackup(tiles),
+                userId,
+            )
+
+            assertThat(largeTiles).isEqualTo(tiles)
+        }
+
+    /**
+     * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+     * one that does, via restore from backup. However, the restore happens after SystemUI's
+     * initialization has set the tiles to default. Note that there's no file in SharedPreferences
+     * to restore.
+     *
+     * The resulting set of large tiles are those that were restored from the backup.
+     */
+    @Test
+    fun restoreFromBackup_afterDefault_noDataInSharedPreferences_allLargeTiles() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+            val tiles = setOf("a", "b", "c").toTileSpecs()
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.RestoreFromBackup(tiles),
+                userId,
+            )
+
+            assertThat(largeTiles).isEqualTo(tiles)
+        }
+
+    /**
+     * This test corresponds to a user that restored from a build that supported different sizes
+     * tiles. First the list of tiles is restored in Settings and then a file containing some large
+     * tiles overrides the current shared preferences file
+     *
+     * The resulting set of large tiles are those that were restored from the shared preferences
+     * backup (and not the full list).
+     */
+    @Test
+    fun restoreFromBackup_thenRestoreOfSharedPrefs_sharedPrefsAreLarge() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val tiles = setOf("a", "b", "c").toTileSpecs()
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.RestoreFromBackup(tiles),
+                userId,
+            )
+
+            val tilesFromBackupOfSharedPrefs = setOf("a")
+            setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs)
+            broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+
+            assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs())
+        }
+
+    /**
+     * This test corresponds to a user that restored from a build that supported different sizes
+     * tiles. However, this restore of settings happened after SystemUI's restore of the SharedPrefs
+     * containing the user's previous selections to large/small tiles.
+     *
+     * The resulting set of large tiles are those that were restored from the shared preferences
+     * backup (and not the full list).
+     */
+    @Test
+    fun restoreFromBackup_afterRestoreOfSharedPrefs_sharedPrefsAreLarge() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val tiles = setOf("a", "b", "c").toTileSpecs()
+            val tilesFromBackupOfSharedPrefs = setOf("a")
+
+            setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs)
+            broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.RestoreFromBackup(tiles),
+                userId,
+            )
+
+            assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs())
+        }
+
+    /**
+     * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+     * one that does, via restore from backup. After that, the user modifies the size of some tiles
+     * and then restarts the device.
+     *
+     * The resulting set of large tiles are those after the user modifications.
+     */
+    @Test
+    fun restoreFromBackup_changeSizes_restart_newLargeSet() =
+        kosmos.runTest {
+            val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+            val readTiles = setOf("a", "b", "c").toTileSpecs()
+
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.RestoreFromBackup(readTiles),
+                userId,
+            )
+            underTest.setLargeTilesSpecs(emptySet())
+
+            assertThat(largeTiles).isEmpty()
+
+            // Restart
+            underTest.setInitialOrUpgradeLargeTilesSpecs(
+                TilesUpgradePath.ReadFromSettings(readTiles),
+                userId,
+            )
+            assertThat(largeTiles).isEmpty()
+        }
+
+    private companion object {
+        private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs"
+
+        private fun Kosmos.getSharedPreferences(): SharedPreferences =
+            userFileManager.getSharedPreferences(
+                QSPreferencesRepository.FILE_NAME,
+                Context.MODE_PRIVATE,
+                userRepository.getSelectedUserInfo().id,
+            )
+
+        private fun Kosmos.setLargeTilesSpecsInSharedPreferences(specs: Set<String>) {
+            getSharedPreferences().edit().putStringSet(LARGE_TILES_SPECS_KEY, specs).apply()
+        }
+
+        private fun Kosmos.getLargeTilesSpecsFromSharedPreferences(): Set<String> {
+            return getSharedPreferences().getStringSet(LARGE_TILES_SPECS_KEY, emptySet())!!
+        }
+
+        private fun Set<String>.toTileSpecs(): Set<TileSpec> {
+            return map { TileSpec.create(it) }.toSet()
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
index c775bfd..9e400a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
@@ -34,6 +35,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
 class GridLayoutTypeInteractorTest : SysuiTestCase() {
     val kosmos = testKosmos()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
index 79acfda..9838bcb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
@@ -66,7 +66,7 @@
                 runCurrent()
 
                 // Resize it to large
-                qsPreferencesRepository.setLargeTilesSpecs(setOf(spec))
+                qsPreferencesRepository.writeLargeTileSpecs(setOf(spec))
                 runCurrent()
 
                 // Assert that the new tile was added to the large tiles set
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
index 2e7aeb4..9fe783b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.panels.data.repository.QSColumnsRepository
@@ -76,6 +77,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun withDualShade_returnsCorrectValue() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
index fdbf42c..d5e502e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
@@ -36,6 +37,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -43,6 +45,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() {
@@ -63,6 +66,7 @@
     }
 
     @Test
+    @EnableSceneContainer
     fun shouldMediaShowInRow() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
index 241cdbf..4912c31 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.testCase
@@ -88,6 +89,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationNull_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
@@ -111,6 +113,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationQS_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
@@ -133,6 +136,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index 4b8cd37..d9b3926 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.res.R
 import com.android.systemui.retail.data.repository.FakeRetailModeRepository
@@ -242,9 +243,12 @@
             storeTilesForUser(startingTiles, userId)
 
             val tiles by collectLastValue(underTest.tilesSpecs(userId))
-            val tilesRead by collectLastValue(underTest.tilesReadFromSetting.consumeAsFlow())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
 
-            assertThat(tilesRead).isEqualTo(startingTiles.toTileSpecs().toSet() to userId)
+            assertThat(tilesRead)
+                .isEqualTo(
+                    TilesUpgradePath.ReadFromSettings(startingTiles.toTileSpecs().toSet()) to userId
+                )
         }
 
     @Test
@@ -258,13 +262,13 @@
             val tiles10 by collectLastValue(underTest.tilesSpecs(10))
             val tiles11 by collectLastValue(underTest.tilesSpecs(11))
 
-            val tilesRead by collectValues(underTest.tilesReadFromSetting.consumeAsFlow())
+            val tilesRead by collectValues(underTest.tilesUpgradePath.consumeAsFlow())
 
             assertThat(tilesRead).hasSize(2)
             assertThat(tilesRead)
                 .containsExactly(
-                    startingTiles10.toTileSpecs().toSet() to 10,
-                    startingTiles11.toTileSpecs().toSet() to 11,
+                    TilesUpgradePath.ReadFromSettings(startingTiles10.toTileSpecs().toSet()) to 10,
+                    TilesUpgradePath.ReadFromSettings(startingTiles11.toTileSpecs().toSet()) to 11,
                 )
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
index 1945f75..29bd18d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
@@ -7,8 +7,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.qs.pipeline.data.model.RestoreData
-import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepositoryTest.Companion.toTilesSet
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
@@ -352,11 +352,11 @@
     @Test
     fun noSettingsStored_noTilesReadFromSettings() =
         testScope.runTest {
-            val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
             val tiles by collectLastValue(underTest.tiles())
 
             assertThat(tiles).isEqualTo(getDefaultTileSpecs())
-            assertThat(tilesRead).isEqualTo(null)
+            assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet)
         }
 
     @Test
@@ -365,19 +365,20 @@
             val storedTiles = "a,b"
             storeTiles(storedTiles)
             val tiles by collectLastValue(underTest.tiles())
-            val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
 
-            assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet())
+            assertThat(tilesRead)
+                .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet()))
         }
 
     @Test
     fun noSettingsStored_tilesChanged_tilesReadFromSettingsNotChanged() =
         testScope.runTest {
-            val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
             val tiles by collectLastValue(underTest.tiles())
 
             underTest.addTile(TileSpec.create("a"))
-            assertThat(tilesRead).isEqualTo(null)
+            assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet)
         }
 
     @Test
@@ -386,10 +387,34 @@
             val storedTiles = "a,b"
             storeTiles(storedTiles)
             val tiles by collectLastValue(underTest.tiles())
-            val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
 
             underTest.addTile(TileSpec.create("c"))
-            assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet())
+            assertThat(tilesRead)
+                .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet()))
+        }
+
+    @Test
+    fun tilesRestoredFromBackup() =
+        testScope.runTest {
+            val specsBeforeRestore = "a,b,c,d,e"
+            val restoredSpecs = "a,c,d,f"
+            val autoAddedBeforeRestore = "b,d"
+            val restoredAutoAdded = "d,e"
+
+            storeTiles(specsBeforeRestore)
+            val tiles by collectLastValue(underTest.tiles())
+            val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
+            runCurrent()
+
+            val restoreData =
+                RestoreData(restoredSpecs.toTileSpecs(), restoredAutoAdded.toTilesSet(), USER)
+            underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet())
+            runCurrent()
+
+            val expected = "a,b,c,d,f"
+            assertThat(tilesRead)
+                .isEqualTo(TilesUpgradePath.RestoreFromBackup(expected.toTilesSet()))
         }
 
     private fun getDefaultTileSpecs(): List<TileSpec> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
index df2dd99..b98059a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
 import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -84,13 +84,14 @@
         }
 
     @Test
-    fun downFromTopLeft_switchesToNotificationsShade() =
+    fun downFromTopStart_switchesToNotificationsShade() =
         testScope.runTest {
             val actions by collectLastValue(underTest.actions)
             underTest.activateIn(this)
 
             val action =
-                (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay)
+                (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf))
+                    as? ShowOverlay)
             assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade)
             val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
             assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index a0d86f2..80c7026 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runCurrent
@@ -60,6 +61,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -713,4 +718,43 @@
             assertThat(currentScene).isEqualTo(originalScene)
             assertThat(currentOverlays).isEmpty()
         }
+
+    @Test
+    fun changeScene_notifiesAboutToChangeListener() =
+        kosmos.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            // Unlock so transitioning to the Gone scene becomes possible.
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            underTest.changeScene(toScene = Scenes.Gone, loggingReason = "")
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+            val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+            underTest.registerSceneStateProcessor(processor)
+
+            underTest.changeScene(
+                toScene = Scenes.Lockscreen,
+                sceneState = KeyguardState.AOD,
+                loggingReason = "",
+            )
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+        }
+
+    @Test
+    fun changeScene_noOp_whenFromAndToAreTheSame() =
+        kosmos.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+            underTest.registerSceneStateProcessor(processor)
+
+            underTest.changeScene(toScene = checkNotNull(currentScene), loggingReason = "")
+
+            verify(processor, never()).onSceneAboutToChange(any(), any())
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
new file mode 100644
index 0000000..a09e5cd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerSwipeDetectorTest : SysuiTestCase() {
+
+    private val edgeSize = 40
+    private val screenWidth = 800
+    private val screenHeight = 600
+
+    private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp)
+
+    @Test
+    fun source_noEdge_detectsLeftHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2)
+        assertThat(detectedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() {
+        val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(LeftEdge)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnTopRight_detectsRightHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(RightHalf)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnTopRight_detectsRightEdge() {
+        val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(RightEdge)
+    }
+
+    @Test
+    fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1)
+        assertThat(detectedEdge).isEqualTo(RightHalf)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnBottom_detectsBottomEdge() {
+        val detectedEdge =
+            swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2))
+        assertThat(detectedEdge).isEqualTo(BottomEdge)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnBottom_detectsLeftHalf() {
+        val detectedEdge =
+            swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1))
+        assertThat(detectedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnLeft_detectsLeftEdge() {
+        val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+        assertThat(detectedEdge).isEqualTo(LeftEdge)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnLeft_detectsLeftHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+        assertThat(detectedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnRight_detectsRightEdge() {
+        val detectedEdge =
+            swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+        assertThat(detectedEdge).isEqualTo(RightEdge)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnRight_detectsRightHalf() {
+        val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+        assertThat(detectedEdge).isEqualTo(RightHalf)
+    }
+
+    @Test
+    fun resolve_startEdgeInLtr_resolvesLeftEdge() {
+        val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(LeftEdge)
+    }
+
+    @Test
+    fun resolve_startEdgeInRtl_resolvesRightEdge() {
+        val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(RightEdge)
+    }
+
+    @Test
+    fun resolve_endEdgeInLtr_resolvesRightEdge() {
+        val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(RightEdge)
+    }
+
+    @Test
+    fun resolve_endEdgeInRtl_resolvesLeftEdge() {
+        val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(LeftEdge)
+    }
+
+    @Test
+    fun resolve_startHalfInLtr_resolvesLeftHalf() {
+        val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(LeftHalf)
+    }
+
+    @Test
+    fun resolve_startHalfInRtl_resolvesRightHalf() {
+        val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(RightHalf)
+    }
+
+    @Test
+    fun resolve_endHalfInLtr_resolvesRightHalf() {
+        val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(RightHalf)
+    }
+
+    @Test
+    fun resolve_endHalfInRtl_resolvesLeftHalf() {
+        val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(LeftHalf)
+    }
+
+    private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+        return swipeFrom(x, y, Orientation.Vertical)
+    }
+
+    private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+        return swipeFrom(x, y, Orientation.Horizontal)
+    }
+
+    private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? {
+        return underTest.source(
+            layoutSize = IntSize(width = screenWidth, height = screenHeight),
+            position = IntOffset(x, y),
+            density = Density(1f),
+            orientation = orientation,
+        )
+    }
+}
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 30d9f73..adaebbd 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
@@ -48,6 +48,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -55,6 +56,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @EnableSceneContainer
@@ -324,7 +326,7 @@
             kosmos.enableSingleShade()
 
             assertThat(shadeMode).isEqualTo(ShadeMode.Single)
-            assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+            assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
         }
 
     @Test
@@ -334,26 +336,28 @@
             kosmos.enableSplitShade()
 
             assertThat(shadeMode).isEqualTo(ShadeMode.Split)
-            assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+            assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
         }
 
     @Test
-    fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+    fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() =
         testScope.runTest {
             val shadeMode by collectLastValue(kosmos.shadeMode)
             kosmos.enableDualShade(wideLayout = false)
 
             assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
-            assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+            assertThat(underTest.swipeSourceDetector)
+                .isInstanceOf(SceneContainerSwipeDetector::class.java)
         }
 
     @Test
-    fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+    fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() =
         testScope.runTest {
             val shadeMode by collectLastValue(kosmos.shadeMode)
             kosmos.enableDualShade(wideLayout = true)
 
             assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
-            assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+            assertThat(underTest.swipeSourceDetector)
+                .isInstanceOf(SceneContainerSwipeDetector::class.java)
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
deleted file mode 100644
index 3d76d28..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SplitEdgeDetectorTest : SysuiTestCase() {
-
-    private val edgeSize = 40
-    private val screenWidth = 800
-    private val screenHeight = 600
-
-    private var edgeSplitFraction = 0.7f
-
-    private val underTest =
-        SplitEdgeDetector(
-            topEdgeSplitFraction = { edgeSplitFraction },
-            edgeSize = edgeSize.dp,
-        )
-
-    @Test
-    fun source_noEdge_detectsNothing() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = screenWidth / 2,
-                y = screenHeight / 2,
-            )
-        assertThat(detectedEdge).isNull()
-    }
-
-    @Test
-    fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(TopLeft)
-    }
-
-    @Test
-    fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
-        val detectedEdge =
-            swipeHorizontallyFrom(
-                x = 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(Left)
-    }
-
-    @Test
-    fun source_swipeVerticallyOnTopRight_detectsTopRight() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = screenWidth - 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(TopRight)
-    }
-
-    @Test
-    fun source_swipeHorizontallyOnTopRight_detectsRight() {
-        val detectedEdge =
-            swipeHorizontallyFrom(
-                x = screenWidth - 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(Right)
-    }
-
-    @Test
-    fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = (screenWidth * edgeSplitFraction).toInt() - 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(TopLeft)
-    }
-
-    @Test
-    fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = (screenWidth * edgeSplitFraction).toInt() + 1,
-                y = edgeSize - 1,
-            )
-        assertThat(detectedEdge).isEqualTo(TopRight)
-    }
-
-    @Test
-    fun source_edgeSplitFractionUpdatesDynamically() {
-        val middleX = (screenWidth * 0.5f).toInt()
-        val topY = 0
-
-        // Split closer to the right; middle of screen is considered "left".
-        edgeSplitFraction = 0.6f
-        assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
-
-        // Split closer to the left; middle of screen is considered "right".
-        edgeSplitFraction = 0.4f
-        assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
-
-        // Illegal fraction.
-        edgeSplitFraction = 1.2f
-        assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
-
-        // Illegal fraction.
-        edgeSplitFraction = -0.3f
-        assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
-    }
-
-    @Test
-    fun source_swipeVerticallyOnBottom_detectsBottom() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = screenWidth / 3,
-                y = screenHeight - (edgeSize / 2),
-            )
-        assertThat(detectedEdge).isEqualTo(Bottom)
-    }
-
-    @Test
-    fun source_swipeHorizontallyOnBottom_detectsNothing() {
-        val detectedEdge =
-            swipeHorizontallyFrom(
-                x = screenWidth / 3,
-                y = screenHeight - (edgeSize - 1),
-            )
-        assertThat(detectedEdge).isNull()
-    }
-
-    @Test
-    fun source_swipeHorizontallyOnLeft_detectsLeft() {
-        val detectedEdge =
-            swipeHorizontallyFrom(
-                x = edgeSize - 1,
-                y = screenHeight / 2,
-            )
-        assertThat(detectedEdge).isEqualTo(Left)
-    }
-
-    @Test
-    fun source_swipeVerticallyOnLeft_detectsNothing() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = edgeSize - 1,
-                y = screenHeight / 2,
-            )
-        assertThat(detectedEdge).isNull()
-    }
-
-    @Test
-    fun source_swipeHorizontallyOnRight_detectsRight() {
-        val detectedEdge =
-            swipeHorizontallyFrom(
-                x = screenWidth - edgeSize + 1,
-                y = screenHeight / 2,
-            )
-        assertThat(detectedEdge).isEqualTo(Right)
-    }
-
-    @Test
-    fun source_swipeVerticallyOnRight_detectsNothing() {
-        val detectedEdge =
-            swipeVerticallyFrom(
-                x = screenWidth - edgeSize + 1,
-                y = screenHeight / 2,
-            )
-        assertThat(detectedEdge).isNull()
-    }
-
-    @Test
-    fun resolve_startInLtr_resolvesLeft() {
-        val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
-        assertThat(resolvedEdge).isEqualTo(Left)
-    }
-
-    @Test
-    fun resolve_startInRtl_resolvesRight() {
-        val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
-        assertThat(resolvedEdge).isEqualTo(Right)
-    }
-
-    @Test
-    fun resolve_endInLtr_resolvesRight() {
-        val resolvedEdge = End.resolve(LayoutDirection.Ltr)
-        assertThat(resolvedEdge).isEqualTo(Right)
-    }
-
-    @Test
-    fun resolve_endInRtl_resolvesLeft() {
-        val resolvedEdge = End.resolve(LayoutDirection.Rtl)
-        assertThat(resolvedEdge).isEqualTo(Left)
-    }
-
-    @Test
-    fun resolve_topStartInLtr_resolvesTopLeft() {
-        val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
-        assertThat(resolvedEdge).isEqualTo(TopLeft)
-    }
-
-    @Test
-    fun resolve_topStartInRtl_resolvesTopRight() {
-        val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
-        assertThat(resolvedEdge).isEqualTo(TopRight)
-    }
-
-    @Test
-    fun resolve_topEndInLtr_resolvesTopRight() {
-        val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
-        assertThat(resolvedEdge).isEqualTo(TopRight)
-    }
-
-    @Test
-    fun resolve_topEndInRtl_resolvesTopLeft() {
-        val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
-        assertThat(resolvedEdge).isEqualTo(TopLeft)
-    }
-
-    private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
-        return swipeFrom(x, y, Orientation.Vertical)
-    }
-
-    private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
-        return swipeFrom(x, y, Orientation.Horizontal)
-    }
-
-    private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
-        return underTest.source(
-            layoutSize = IntSize(width = screenWidth, height = screenHeight),
-            position = IntOffset(x, y),
-            density = Density(1f),
-            orientation = orientation,
-        )
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
index c1477fe..a9f3a65 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
@@ -80,6 +80,8 @@
         // Set desktop mode supported
         whenever(mContext.resources).thenReturn(mResources)
         whenever(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true)
+        whenever(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops))
+            .thenReturn(true)
 
         policy = WorkProfilePolicy(kosmos.profileTypeRepository, mContext)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
index 668f568..d26e195 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
@@ -31,6 +32,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
 class ShadeModeInteractorImplTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
@@ -80,7 +82,7 @@
         }
 
     @Test
-    fun isDualShade_settingEnabled_returnsTrue() =
+    fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() =
         testScope.runTest {
             // TODO(b/391578667): Add a test case for user switching once the bug is fixed.
             val shadeMode by collectLastValue(underTest.shadeMode)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index b8f66ac..dde8678 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 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.TestScope
@@ -59,6 +60,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -103,6 +105,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun hydrateShadeMode_dualShadeEnabled() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d31787..c6801f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -460,14 +460,14 @@
     }
 
     @Test
-    public void testonDisplayAddSystemDecorations() {
+    public void testOnDisplayAddSystemDecorations() {
         mCommandQueue.onDisplayAddSystemDecorations(DEFAULT_DISPLAY);
         waitForIdleSync();
         verify(mCallbacks).onDisplayAddSystemDecorations(eq(DEFAULT_DISPLAY));
     }
 
     @Test
-    public void testonDisplayAddSystemDecorationsForSecondaryDisplay() {
+    public void testOnDisplayAddSystemDecorationsForSecondaryDisplay() {
         mCommandQueue.onDisplayAddSystemDecorations(SECONDARY_DISPLAY);
         waitForIdleSync();
         verify(mCallbacks).onDisplayAddSystemDecorations(eq(SECONDARY_DISPLAY));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index dbe8f82..c7b3175 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -265,91 +264,25 @@
         }
 
     @Test
-    fun chip_positiveStartTime_notPromoted_colorsAreThemed() =
+    fun chip_positiveStartTime_colorsAreAccentThemed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(inCallModel(startTimeMs = 1000, promotedContent = null))
 
             assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(ColorsModel.Themed)
+                .isEqualTo(ColorsModel.AccentThemed)
         }
 
     @Test
-    fun chip_zeroStartTime_notPromoted_colorsAreThemed() =
+    fun chip_zeroStartTime_colorsAreAccentThemed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(inCallModel(startTimeMs = 0, promotedContent = null))
 
             assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(ColorsModel.Themed)
-        }
-
-    @Test
-    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun chip_positiveStartTime_promoted_notifChipsFlagOff_colorsAreThemed() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR)
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(ColorsModel.Themed)
-        }
-
-    @Test
-    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreThemed() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR)
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(ColorsModel.Themed)
-        }
-
-    @Test
-    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun chip_positiveStartTime_promoted_notifChipsFlagOn_colorsAreCustom() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR)
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(
-                    ColorsModel.Custom(
-                        backgroundColorInt = PROMOTED_BACKGROUND_COLOR,
-                        primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR,
-                    )
-                )
-        }
-
-    @Test
-    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreCustom() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR)
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Active).colors)
-                .isEqualTo(
-                    ColorsModel.Custom(
-                        backgroundColorInt = PROMOTED_BACKGROUND_COLOR,
-                        primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR,
-                    )
-                )
+                .isEqualTo(ColorsModel.AccentThemed)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 192ad87..aaa9b58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -186,7 +186,7 @@
 
     @Test
     @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
-    fun chips_onePromotedNotif_colorMatches() =
+    fun chips_onePromotedNotif_colorIsSystemThemed() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -209,10 +209,7 @@
             )
 
             assertThat(latest).hasSize(1)
-            val colors = latest!![0].colors
-            assertThat(colors).isInstanceOf(ColorsModel.Custom::class.java)
-            assertThat((colors as ColorsModel.Custom).backgroundColorInt).isEqualTo(56)
-            assertThat((colors as ColorsModel.Custom).primaryTextColorInt).isEqualTo(89)
+            assertThat(latest!![0].colors).isEqualTo(ColorsModel.SystemThemed)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt
index d727089..9ec5a42 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt
@@ -52,24 +52,13 @@
     }
 
     @Test
-    fun shouldShowText_desiredSlightlyLargerThanMax_true() {
+    fun shouldShowText_desiredMoreThanMax_false() {
         val result =
             underTest.shouldShowText(
                 desiredTextWidthPx = (MAX_WIDTH * 1.1).toInt(),
                 widthMeasureSpec = UNLIMITED_WIDTH_SPEC,
             )
 
-        assertThat(result).isTrue()
-    }
-
-    @Test
-    fun shouldShowText_desiredMoreThanTwiceMax_false() {
-        val result =
-            underTest.shouldShowText(
-                desiredTextWidthPx = (MAX_WIDTH * 2.2).toInt(),
-                widthMeasureSpec = UNLIMITED_WIDTH_SPEC,
-            )
-
         assertThat(result).isFalse()
     }
 
@@ -80,8 +69,8 @@
                 View.MeasureSpec.makeMeasureSpec(MAX_WIDTH / 2, View.MeasureSpec.AT_MOST)
             )
 
-        // WHEN desired is more than twice the smallerWidthSpec
-        val desiredWidth = (MAX_WIDTH * 1.1).toInt()
+        // WHEN desired is more than the smallerWidthSpec
+        val desiredWidth = ((MAX_WIDTH / 2) * 1.1).toInt()
 
         val result =
             underTest.shouldShowText(
@@ -100,8 +89,8 @@
                 View.MeasureSpec.makeMeasureSpec(MAX_WIDTH * 3, View.MeasureSpec.AT_MOST)
             )
 
-        // WHEN desired is more than twice the max
-        val desiredWidth = (MAX_WIDTH * 2.2).toInt()
+        // WHEN desired is more than the max
+        val desiredWidth = (MAX_WIDTH * 1.1).toInt()
 
         val result =
             underTest.shouldShowText(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
index 60030ad..e3a84fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
@@ -54,7 +54,7 @@
                 OngoingActivityChipModel.Active.Timer(
                     key = KEY,
                     icon = createIcon(R.drawable.ic_cake),
-                    colors = ColorsModel.Themed,
+                    colors = ColorsModel.AccentThemed,
                     startTimeMs = 100L,
                     onClickListenerLegacy = null,
                     clickBehavior = OngoingActivityChipModel.ClickBehavior.None,
@@ -68,7 +68,7 @@
                 OngoingActivityChipModel.Active.IconOnly(
                     key = KEY,
                     icon = createIcon(R.drawable.ic_hotspot),
-                    colors = ColorsModel.Themed,
+                    colors = ColorsModel.AccentThemed,
                     onClickListenerLegacy = null,
                     clickBehavior = OngoingActivityChipModel.ClickBehavior.None,
                 )
@@ -90,7 +90,7 @@
                 OngoingActivityChipModel.Active.Timer(
                     key = KEY,
                     icon = createIcon(R.drawable.ic_cake),
-                    colors = ColorsModel.Themed,
+                    colors = ColorsModel.AccentThemed,
                     startTimeMs = 100L,
                     onClickListenerLegacy = null,
                     clickBehavior = OngoingActivityChipModel.ClickBehavior.None,
@@ -132,7 +132,7 @@
                 OngoingActivityChipModel.Active.Timer(
                     key = KEY,
                     icon = createIcon(R.drawable.ic_cake),
-                    colors = ColorsModel.Themed,
+                    colors = ColorsModel.AccentThemed,
                     startTimeMs = 100L,
                     onClickListenerLegacy = null,
                     clickBehavior = OngoingActivityChipModel.ClickBehavior.None,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 816df01..20637cd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -61,14 +61,14 @@
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.addNotifs
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
-import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
-import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -93,7 +93,6 @@
 
     private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
     private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
-    private val callRepo = kosmos.ongoingCallRepository
     private val activeNotificationListRepository = kosmos.activeNotificationListRepository
 
     private val mockSystemUIDialog = mock<SystemUIDialog>()
@@ -132,7 +131,7 @@
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -145,7 +144,7 @@
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -178,7 +177,7 @@
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -191,7 +190,7 @@
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -224,7 +223,7 @@
     fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            addOngoingCallState("call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -237,9 +236,7 @@
         kosmos.runTest {
             val callNotificationKey = "call"
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -255,16 +252,7 @@
         kosmos.runTest {
             val callNotificationKey = "call"
             screenRecordState.value = ScreenRecordModel.Recording
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = "call",
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    )
-                )
-            )
+            addOngoingCallState(callNotificationKey)
 
             val latest by collectLastValue(underTest.chips)
             val unused by collectLastValue(underTest.chipsLegacy)
@@ -281,7 +269,7 @@
     @Test
     fun chipsLegacy_oneChip_notSquished() =
         kosmos.runTest {
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState()
 
             val latest by collectLastValue(underTest.chipsLegacy)
 
@@ -294,17 +282,7 @@
     @Test
     fun chips_oneChip_notSquished() =
         kosmos.runTest {
-            val callNotificationKey = "call"
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = callNotificationKey,
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    )
-                )
-            )
+            addOngoingCallState()
 
             val latest by collectLastValue(underTest.chips)
 
@@ -315,10 +293,10 @@
 
     @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     @Test
-    fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() =
+    fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
 
@@ -329,12 +307,28 @@
                 .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
         }
 
+    @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+    @Test
+    fun chips_twoTimerChips_isSmallPortrait_bothSquished() =
+        kosmos.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            addOngoingCallState(key = "call")
+
+            val latest by collectLastValue(underTest.chips)
+
+            // Squished chips are icon only
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+            assertThat(latest!!.active[1])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+        }
+
     @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     @Test
     fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
 
@@ -346,6 +340,23 @@
                 .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
         }
 
+    @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+    @Test
+    fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
+        kosmos.runTest {
+            screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
+            addOngoingCallState(key = "call")
+
+            val latest by collectLastValue(underTest.chips)
+
+            // The screen record countdown isn't squished to icon-only
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java)
+            // But the call chip *is* squished
+            assertThat(latest!!.active[1])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+        }
+
     @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     @Test
     fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() =
@@ -354,7 +365,7 @@
 
             // WHEN there's only one chip
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             // The screen record isn't squished because it's the only one
             assertThat(latest!!.primary)
@@ -363,7 +374,7 @@
                 .isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
 
             // WHEN there's 2 chips
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState(key = "call")
 
             // THEN they both become squished
             assertThat(latest!!.primary)
@@ -382,12 +393,44 @@
                 .isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
         }
 
+    @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+    @Test
+    fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            // WHEN there's only one chip
+            screenRecordState.value = ScreenRecordModel.Recording
+            removeOngoingCallState(key = "call")
+
+            // The screen record isn't squished because it's the only one
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+
+            // WHEN there's 2 chips
+            addOngoingCallState(key = "call")
+
+            // THEN they both become squished
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+            // But the call chip *is* squished
+            assertThat(latest!!.active[1])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+
+            // WHEN we go back down to 1 chip
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            // THEN the remaining chip unsquishes
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+        }
+
     @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     @Test
     fun chipsLegacy_twoChips_isLandscape_notSquished() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState(key = "call")
 
             // WHEN we're in landscape
             val config =
@@ -405,12 +448,35 @@
                 .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
         }
 
+    @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+    @Test
+    fun chips_twoChips_isLandscape_notSquished() =
+        kosmos.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            addOngoingCallState(key = "call")
+
+            // WHEN we're in landscape
+            val config =
+                Configuration(kosmos.mainResources.configuration).apply {
+                    orientation = Configuration.ORIENTATION_LANDSCAPE
+                }
+            kosmos.fakeConfigurationRepository.onConfigurationChange(config)
+
+            val latest by collectLastValue(underTest.chips)
+
+            // THEN the chips aren't squished (squished chips would be icon only)
+            assertThat(latest!!.active[0])
+                .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+            assertThat(latest!!.active[1])
+                .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+        }
+
     @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     @Test
     fun chipsLegacy_twoChips_isLargeScreen_notSquished() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+            addOngoingCallState(key = "call")
 
             // WHEN we're on a large screen
             kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -424,25 +490,19 @@
                 .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
         }
 
-    @Test
     @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
-    fun chips_twoChips_chipsModernizationEnabled_notSquished() =
+    @Test
+    fun chips_twoChips_isLargeScreen_notSquished() =
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = "call",
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    )
-                )
-            )
+            addOngoingCallState(key = "call")
+
+            // WHEN we're on a large screen
+            kosmos.displayStateRepository.setIsLargeScreen(true)
 
             val latest by collectLastValue(underTest.chips)
 
-            // Squished chips would be icon only
+            // THEN the chips aren't squished (squished chips would be icon only)
             assertThat(latest!!.active[0])
                 .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
             assertThat(latest!!.active[1])
@@ -455,7 +515,7 @@
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -469,7 +529,7 @@
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -510,7 +570,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            addOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -525,9 +585,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -545,16 +603,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = callNotificationKey,
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    )
-                )
-            )
+            addOngoingCallState(key = callNotificationKey)
 
             val latest by collectLastValue(underTest.chips)
             val unused by collectLastValue(underTest.chipsLegacy)
@@ -575,9 +624,7 @@
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             val callNotificationKey = "call"
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(key = callNotificationKey)
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -593,9 +640,7 @@
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(key = callNotificationKey)
 
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
@@ -614,16 +659,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = callNotificationKey,
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    )
-                )
-            )
+            addOngoingCallState(key = callNotificationKey)
 
             val latest by collectLastValue(underTest.chips)
             val unused by collectLastValue(underTest.chipsLegacy)
@@ -837,12 +873,10 @@
             val unused by collectLastValue(underTest.chips)
 
             val callNotificationKey = "call"
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
 
             val firstIcon = createStatusBarIconViewOrNull()
-            setNotifs(
+            activeNotificationListRepository.addNotifs(
                 listOf(
                     activeNotificationModel(
                         key = "firstNotif",
@@ -874,14 +908,10 @@
             val callNotificationKey = "call"
             val firstIcon = createStatusBarIconViewOrNull()
             val secondIcon = createStatusBarIconViewOrNull()
-            setNotifs(
+            addOngoingCallState(key = callNotificationKey)
+            activeNotificationListRepository.addNotifs(
                 listOf(
                     activeNotificationModel(
-                        key = callNotificationKey,
-                        whenTime = 499,
-                        callType = CallType.Ongoing,
-                    ),
-                    activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
                         promotedContent =
@@ -913,17 +943,13 @@
             val latest by collectLastValue(underTest.chipsLegacy)
             val unused by collectLastValue(underTest.chips)
 
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
             screenRecordState.value = ScreenRecordModel.Recording
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = "notif",
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
-                    )
+            activeNotificationListRepository.addNotif(
+                activeNotificationModel(
+                    key = "notif",
+                    statusBarChipIcon = createStatusBarIconViewOrNull(),
+                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                 )
             )
 
@@ -942,20 +968,14 @@
             val callNotificationKey = "call"
             val notifIcon = createStatusBarIconViewOrNull()
             screenRecordState.value = ScreenRecordModel.Recording
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = callNotificationKey,
-                        whenTime = 499,
-                        callType = CallType.Ongoing,
-                    ),
-                    activeNotificationModel(
-                        key = "notif",
-                        statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
-                    ),
+            activeNotificationListRepository.addNotif(
+                activeNotificationModel(
+                    key = "notif",
+                    statusBarChipIcon = notifIcon,
+                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                 )
             )
+            addOngoingCallState(key = callNotificationKey)
 
             assertThat(latest!!.active.size).isEqualTo(2)
             assertIsScreenRecordChip(latest!!.active[0])
@@ -982,7 +1002,7 @@
                 )
             )
             // And everything else hidden
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = callNotificationKey)
             mediaProjectionState.value = MediaProjectionState.NotProjecting
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
@@ -991,9 +1011,7 @@
             assertIsNotifChip(latest, context, notifIcon, "notif")
 
             // WHEN the higher priority call chip is added
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
 
             // THEN the higher priority call chip is used
             assertIsCallChip(latest, callNotificationKey)
@@ -1024,17 +1042,13 @@
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
             val notifIcon = createStatusBarIconViewOrNull()
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = "notif",
-                        statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
-                    )
+            activeNotificationListRepository.addNotif(
+                activeNotificationModel(
+                    key = "notif",
+                    statusBarChipIcon = notifIcon,
+                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                 )
             )
 
@@ -1056,7 +1070,7 @@
             assertIsCallChip(latest, callNotificationKey)
 
             // WHEN the higher priority call is removed
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = callNotificationKey)
 
             // THEN the lower priority notif is used
             assertIsNotifChip(latest, context, notifIcon, "notif")
@@ -1069,17 +1083,15 @@
             val callNotificationKey = "call"
             // Start with just the lowest priority chip shown
             val notifIcon = createStatusBarIconViewOrNull()
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = "notif",
-                        statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
-                    )
+            activeNotificationListRepository.addNotif(
+                activeNotificationModel(
+                    key = "notif",
+                    statusBarChipIcon = notifIcon,
+                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                 )
             )
             // And everything else hidden
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = callNotificationKey)
             mediaProjectionState.value = MediaProjectionState.NotProjecting
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
@@ -1092,9 +1104,7 @@
             assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
 
             // WHEN the higher priority call chip is added
-            callRepo.setOngoingCallState(
-                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
-            )
+            addOngoingCallState(callNotificationKey)
 
             // THEN the higher priority call chip is used as primary and notif is demoted to
             // secondary
@@ -1125,7 +1135,7 @@
 
             // WHEN screen record and call is dropped
             screenRecordState.value = ScreenRecordModel.DoingNothing
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = callNotificationKey)
 
             // THEN media projection and notif remain
             assertIsShareToAppChip(latest!!.primary)
@@ -1172,21 +1182,7 @@
             assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
 
             // WHEN the higher priority call chip is added
-            setNotifs(
-                listOf(
-                    activeNotificationModel(
-                        key = callNotificationKey,
-                        statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        callType = CallType.Ongoing,
-                        whenTime = 499,
-                    ),
-                    activeNotificationModel(
-                        key = "notif",
-                        statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
-                    ),
-                )
-            )
+            addOngoingCallState(key = callNotificationKey)
 
             // THEN the higher priority call chip and notif are active in that order
             assertThat(latest!!.active.size).isEqualTo(2)
@@ -1372,7 +1368,7 @@
         kosmos.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value = MediaProjectionState.NotProjecting
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -1399,7 +1395,7 @@
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
             screenRecordState.value = ScreenRecordModel.DoingNothing
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            removeOngoingCallState(key = "call")
 
             val latest by collectLastValue(underTest.primaryChip)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
index 8eea2a8..048028c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
@@ -21,52 +21,44 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.statusbar.lockscreenShadeTransitionController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.isNull
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class NotificationShelfInteractorTest : SysuiTestCase() {
 
-    private val keyguardRepository = FakeKeyguardRepository()
-    private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-
-    private val screenOffAnimationController =
-        mock<ScreenOffAnimationController>().also {
-            whenever(it.allowWakeUpIfDozing()).thenReturn(true)
+    private val kosmos =
+        Kosmos().apply {
+            testCase = this@NotificationShelfInteractorTest
+            lockscreenShadeTransitionController = mock()
+            screenOffAnimationController = mock()
+            statusBarStateController = mock()
+            whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
         }
-    private val statusBarStateController: StatusBarStateController = mock()
-    private val powerRepository = FakePowerRepository()
-    private val powerInteractor =
-        PowerInteractorFactory.create(
-                repository = powerRepository,
-                screenOffAnimationController = screenOffAnimationController,
-                statusBarStateController = statusBarStateController,
-            )
-            .powerInteractor
+    private val underTest = kosmos.notificationShelfInteractor
 
-    private val keyguardTransitionController: LockscreenShadeTransitionController = mock()
-    private val underTest =
-        NotificationShelfInteractor(
-            keyguardRepository,
-            deviceEntryFaceAuthRepository,
-            powerInteractor,
-            keyguardTransitionController,
-        )
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
+
+    private val statusBarStateController = kosmos.statusBarStateController
+    private val powerRepository = kosmos.fakePowerRepository
+    private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController
 
     @Test
     fun shelfIsNotStatic_whenKeyguardNotShowing() = runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index e2fb3ba..6381b4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -19,73 +19,54 @@
 import android.os.PowerManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.runTest
-import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.enableSingleShade
+import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.statusbar.lockscreenShadeTransitionController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
+import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class NotificationShelfViewModelTest : SysuiTestCase() {
 
-    @Component(modules = [SysUITestModule::class, ActivatableNotificationViewModelModule::class])
-    @SysUISingleton
-    interface TestComponent : SysUITestComponent<NotificationShelfViewModel> {
-
-        val deviceEntryFaceAuthRepository: FakeDeviceEntryFaceAuthRepository
-        val keyguardRepository: FakeKeyguardRepository
-        val powerRepository: FakePowerRepository
-
-        @Component.Factory
-        interface Factory {
-            fun create(
-                @BindsInstance test: SysuiTestCase,
-                mocks: TestMocksModule,
-            ): TestComponent
+    private val kosmos =
+        Kosmos().apply {
+            testCase = this@NotificationShelfViewModelTest
+            lockscreenShadeTransitionController = mock()
+            screenOffAnimationController = mock()
+            statusBarStateController = mock()
+            whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
         }
-    }
+    private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val powerRepository = kosmos.fakePowerRepository
+    private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController }
 
-    private val keyguardTransitionController: LockscreenShadeTransitionController = mock()
-    private val screenOffAnimationController: ScreenOffAnimationController = mock {
-        whenever(allowWakeUpIfDozing()).thenReturn(true)
-    }
-    private val statusBarStateController: SysuiStatusBarStateController = mock()
-
-    private val testComponent: TestComponent =
-        DaggerNotificationShelfViewModelTest_TestComponent.factory()
-            .create(
-                test = this,
-                mocks =
-                    TestMocksModule(
-                        lockscreenShadeTransitionController = keyguardTransitionController,
-                        screenOffAnimationController = screenOffAnimationController,
-                        statusBarStateController = statusBarStateController,
-                    )
-            )
+    private val underTest by lazy { kosmos.notificationShelfViewModel }
 
     @Test
     fun canModifyColorOfNotifications_whenKeyguardNotShowing() =
-        testComponent.runTest {
+        kosmos.runTest {
             val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
             keyguardRepository.setKeyguardShowing(false)
@@ -95,7 +76,7 @@
 
     @Test
     fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() =
-        testComponent.runTest {
+        kosmos.runTest {
             val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
             keyguardRepository.setKeyguardShowing(true)
@@ -106,7 +87,7 @@
 
     @Test
     fun cannotModifyColorOfNotifications_whenBypass() =
-        testComponent.runTest {
+        kosmos.runTest {
             val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
             keyguardRepository.setKeyguardShowing(true)
@@ -117,7 +98,7 @@
 
     @Test
     fun isClickable_whenKeyguardShowing() =
-        testComponent.runTest {
+        kosmos.runTest {
             val isClickable by collectLastValue(underTest.isClickable)
 
             keyguardRepository.setKeyguardShowing(true)
@@ -127,7 +108,7 @@
 
     @Test
     fun isNotClickable_whenKeyguardNotShowing() =
-        testComponent.runTest {
+        kosmos.runTest {
             val isClickable by collectLastValue(underTest.isClickable)
 
             keyguardRepository.setKeyguardShowing(false)
@@ -137,7 +118,7 @@
 
     @Test
     fun onClicked_goesToLockedShade() =
-        with(testComponent) {
+        kosmos.runTest {
             whenever(statusBarStateController.isDozing).thenReturn(true)
 
             underTest.onShelfClicked()
@@ -146,4 +127,48 @@
             assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
             verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
         }
+
+    @Test
+    @EnableSceneContainer
+    fun isAlignedToEnd_splitShade_true() =
+        kosmos.runTest {
+            val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+            kosmos.enableSplitShade()
+
+            assertThat(isShelfAlignedToEnd).isTrue()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun isAlignedToEnd_singleShade_false() =
+        kosmos.runTest {
+            val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+            kosmos.enableSingleShade()
+
+            assertThat(isShelfAlignedToEnd).isFalse()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun isAlignedToEnd_dualShade_wideScreen_false() =
+        kosmos.runTest {
+            val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+            kosmos.enableDualShade(wideLayout = true)
+
+            assertThat(isShelfAlignedToEnd).isFalse()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun isAlignedToEnd_dualShade_narrowScreen_false() =
+        kosmos.runTest {
+            val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+            kosmos.enableDualShade(wideLayout = false)
+
+            assertThat(isShelfAlignedToEnd).isFalse()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
index d14ff35..e5cb0fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
@@ -49,6 +49,7 @@
     private val sectionsManager = mock<NotificationSectionsManager>()
     private val msdlPlayer = kosmos.fakeMSDLPlayer
     private var canRowBeDismissed = true
+    private var magneticAnimationsCancelled = false
 
     private val underTest = kosmos.magneticNotificationRowManagerImpl
 
@@ -64,6 +65,7 @@
         children = notificationTestHelper.createGroup(childrenNumber).childrenContainer
         swipedRow = children.attachedChildren[childrenNumber / 2]
         configureMagneticRowListener(swipedRow)
+        magneticAnimationsCancelled = false
     }
 
     @Test
@@ -247,6 +249,35 @@
             assertThat(underTest.currentState).isEqualTo(State.IDLE)
         }
 
+    @Test
+    fun onMagneticInteractionEnd_whenDetached_cancelsMagneticAnimations() =
+        kosmos.testScope.runTest {
+            // GIVEN the swiped row is detached
+            setDetachedState()
+
+            // WHEN the interaction ends on the row
+            underTest.onMagneticInteractionEnd(swipedRow, velocity = null)
+
+            // THEN magnetic animations are cancelled
+            assertThat(magneticAnimationsCancelled).isTrue()
+        }
+
+    @Test
+    fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() =
+        kosmos.testScope.runTest {
+            val neighborRow = children.attachedChildren[childrenNumber / 2 - 1]
+            configureMagneticRowListener(neighborRow)
+
+            // GIVEN that targets are set
+            setTargets()
+
+            // WHEN the interactionEnd is called on a target different from the swiped row
+            underTest.onMagneticInteractionEnd(neighborRow, null)
+
+            // THEN magnetic animations are cancelled
+            assertThat(magneticAnimationsCancelled).isTrue()
+        }
+
     private fun setDetachedState() {
         val threshold = 100f
         underTest.setSwipeThresholdPx(threshold)
@@ -284,7 +315,11 @@
                     startVelocity: Float,
                 ) {}
 
-                override fun cancelMagneticAnimations() {}
+                override fun cancelMagneticAnimations() {
+                    magneticAnimationsCancelled = true
+                }
+
+                override fun cancelTranslationAnimations() {}
 
                 override fun canRowBeDismissed(): Boolean = canRowBeDismissed
             }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 766ae73..789701f5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -405,7 +405,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
         mSwipeHelper.snapChild(mNotificationRow, 0, 0);
 
-        verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
         verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
         verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
     }
@@ -416,7 +416,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
         mSwipeHelper.snapChild(mNotificationRow, 10, 0);
 
-        verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
         verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
         verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
     }
@@ -426,7 +426,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
         mSwipeHelper.snapChild(mView, 10, 0);
 
-        verify(mCallback).onDragCancelledWithVelocity(mView, 0);
+        verify(mCallback).onDragCancelled(mView);
         verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 57b7df7..31f8590 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -51,11 +51,13 @@
 import com.android.systemui.shade.ShadeViewStateProvider
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.core.NewStatusBarIcons
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
 import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -85,6 +87,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
+@DisableFlags(NewStatusBarIcons.FLAG_NAME)
 class KeyguardStatusBarViewControllerTest : SysuiTestCase() {
     private lateinit var kosmos: Kosmos
     private lateinit var testScope: TestScope
@@ -190,6 +193,7 @@
             statusBarIconController,
             iconManagerFactory,
             batteryMeterViewController,
+            kosmos.batteryViewModelFactory,
             shadeViewStateProvider,
             keyguardStateController,
             keyguardBypassController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 2e12336..6f785a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
 import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
 
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
 import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
 
 import static org.junit.Assert.assertFalse;
@@ -76,6 +77,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.bouncer.ui.BouncerView;
 import com.android.systemui.bouncer.ui.BouncerViewDelegate;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
@@ -171,6 +173,7 @@
     @Mock private SceneInteractor mSceneInteractor;
     @Mock private DismissCallbackRegistry mDismissCallbackRegistry;
     @Mock private BouncerInteractor mBouncerInteractor;
+    @Mock private CommunalSceneInteractor mCommunalSceneInteractor;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
@@ -209,6 +212,7 @@
                 .thenReturn(mNotificationShadeWindowView);
         when(mNotificationShadeWindowView.getWindowInsetsController())
                 .thenReturn(mWindowInsetsController);
+        when(mCommunalSceneInteractor.isIdleOnCommunal()).thenReturn(MutableStateFlow(false));
 
         mStatusBarKeyguardViewManager =
                 new StatusBarKeyguardViewManager(
@@ -245,7 +249,8 @@
                         mExecutor,
                         () -> mDeviceEntryInteractor,
                         mDismissCallbackRegistry,
-                        () -> mBouncerInteractor) {
+                        () -> mBouncerInteractor,
+                        mCommunalSceneInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -749,7 +754,8 @@
                         mExecutor,
                         () -> mDeviceEntryInteractor,
                         mDismissCallbackRegistry,
-                        () -> mBouncerInteractor) {
+                        () -> mBouncerInteractor,
+                        mCommunalSceneInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 73c191b..c48287c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor
 
 import android.app.PendingIntent
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -30,12 +31,12 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
 import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -50,6 +51,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarChipsModernization.FLAG_NAME)
 class OngoingCallInteractorTest : SysuiTestCase() {
     private val kosmos = Kosmos().useUnconfinedTestDispatcher()
     private val repository = kosmos.activeNotificationListRepository
@@ -76,21 +78,13 @@
             val testIntent: PendingIntent = mock()
             val testPromotedContent =
                 PromotedNotificationContentModel.Builder("promotedCall").build()
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "promotedCall",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                statusBarChipIcon = testIconView,
-                                contentIntent = testIntent,
-                                promotedContent = testPromotedContent,
-                            )
-                        )
-                    }
-                    .build()
+            addOngoingCallState(
+                key = "promotedCall",
+                startTimeMs = 1000L,
+                statusBarChipIconView = testIconView,
+                contentIntent = testIntent,
+                promotedContent = testPromotedContent,
+            )
 
             // Verify model is InCall and has the correct icon, intent, and promoted content.
             assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
@@ -101,45 +95,13 @@
         }
 
     @Test
-    fun ongoingCallNotification_emitsInCall() =
-        kosmos.runTest {
-            val latest by collectLastValue(underTest.ongoingCallState)
-
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                            )
-                        )
-                    }
-                    .build()
-
-            assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
-        }
-
-    @Test
     fun notificationRemoved_emitsNoCall() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.ongoingCallState)
 
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                            )
-                        )
-                    }
-                    .build()
+            addOngoingCallState(key = "testKey")
+            removeOngoingCallState(key = "testKey")
 
-            repository.activeNotifications.value = ActiveNotificationsStore()
             assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java)
         }
 
@@ -149,19 +111,7 @@
             kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
             val latest by collectLastValue(underTest.ongoingCallState)
 
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+            addOngoingCallState(uid = UID)
 
             assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
         }
@@ -172,19 +122,7 @@
             kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
             val latest by collectLastValue(underTest.ongoingCallState)
 
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+            addOngoingCallState(uid = UID)
 
             assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
         }
@@ -196,19 +134,7 @@
 
             // Start with notification and app not visible
             kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+            addOngoingCallState(uid = UID)
             assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
 
             // App becomes visible
@@ -234,7 +160,7 @@
                     kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
                         .ongoingProcessRequiresStatusBarVisible
                 )
-            postOngoingCallNotification()
+            addOngoingCallState()
 
             assertThat(isStatusBarRequired).isTrue()
             assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -256,9 +182,9 @@
                         .ongoingProcessRequiresStatusBarVisible
                 )
 
-            postOngoingCallNotification()
+            addOngoingCallState(key = "testKey")
 
-            repository.activeNotifications.value = ActiveNotificationsStore()
+            removeOngoingCallState(key = "testKey")
 
             assertThat(isStatusBarRequired).isFalse()
             assertThat(requiresStatusBarVisibleInRepository).isFalse()
@@ -283,7 +209,7 @@
 
             kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
 
-            postOngoingCallNotification()
+            addOngoingCallState(uid = UID)
 
             assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
             assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -305,7 +231,7 @@
             clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
             // Set up notification but not in fullscreen
             kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
-            postOngoingCallNotification()
+            addOngoingCallState()
 
             assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
             verify(kosmos.swipeStatusBarAwayGestureHandler, never())
@@ -319,7 +245,7 @@
 
             // Set up notification and fullscreen mode
             kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-            postOngoingCallNotification()
+            addOngoingCallState()
 
             assertThat(isGestureListeningEnabled).isTrue()
             verify(kosmos.swipeStatusBarAwayGestureHandler)
@@ -333,7 +259,7 @@
 
             // Set up notification and fullscreen mode
             kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-            postOngoingCallNotification()
+            addOngoingCallState()
 
             clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
 
@@ -360,7 +286,7 @@
                 )
 
             // Start with an ongoing call (which should set status bar required)
-            postOngoingCallNotification()
+            addOngoingCallState()
 
             assertThat(isStatusBarRequiredForOngoingCall).isTrue()
             assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -374,22 +300,6 @@
             assertThat(requiresStatusBarVisibleInWindowController).isFalse()
         }
 
-    private fun postOngoingCallNotification() {
-        repository.activeNotifications.value =
-            ActiveNotificationsStore.Builder()
-                .apply {
-                    addIndividualNotif(
-                        activeNotificationModel(
-                            key = "notif1",
-                            whenTime = 1000L,
-                            callType = CallType.Ongoing,
-                            uid = UID,
-                        )
-                    )
-                }
-                .build()
-    }
-
     companion object {
         private const val UID = 885
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index 4759c08..183cd8f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -19,6 +19,9 @@
 import android.graphics.Color
 import android.graphics.Rect
 import android.view.View
+import androidx.compose.runtime.getValue
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
@@ -26,15 +29,20 @@
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
+import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
 import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel
 import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import org.mockito.Mockito.mock
 
 class FakeHomeStatusBarViewModel(
     override val operatorNameViewModel: StatusBarOperatorNameViewModel
-) : HomeStatusBarViewModel {
+) : HomeStatusBarViewModel, ExclusiveActivatable() {
+    private val hydrator = Hydrator("FakeHomeStatusBarViewModel.hydrator")
+
     override val areNotificationsLightsOut = MutableStateFlow(false)
 
     override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false)
@@ -56,6 +64,11 @@
 
     override val isHomeStatusBarAllowedByScene = MutableStateFlow(false)
 
+    override val batteryViewModelFactory: BatteryViewModel.Factory =
+        object : BatteryViewModel.Factory {
+            override fun create(): BatteryViewModel = mock(BatteryViewModel::class.java)
+        }
+
     override val shouldShowOperatorNameView = MutableStateFlow(false)
 
     override val isClockVisible =
@@ -80,6 +93,7 @@
 
     var darkIconTint = Color.BLACK
     var lightIconTint = Color.WHITE
+    var darkIntensity = 0f
 
     override val areaTint: Flow<StatusBarTintColor> =
         MutableStateFlow(
@@ -91,4 +105,22 @@
                 }
             }
         )
+
+    val isAreaDarkSource =
+        MutableStateFlow(
+            IsAreaDark { viewBounds ->
+                if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) {
+                    darkIntensity < 0.5f
+                } else {
+                    false
+                }
+            }
+        )
+
+    override val areaDark: IsAreaDark by
+        hydrator.hydratedStateOf(traceName = "areaDark", source = isAreaDarkSource)
+
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
index fecf1fd..36e18e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
@@ -20,9 +20,12 @@
 import android.content.res.Resources
 import android.hardware.devicestate.DeviceStateManager
 import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD
+import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.R
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
@@ -44,23 +47,26 @@
 import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
 import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import com.android.systemui.unfoldedDeviceState
 import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -72,7 +78,10 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verifyNoMoreInteractions
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -88,6 +97,7 @@
     private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
     private val keyguardInteractor = mock<KeyguardInteractor>()
     private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>()
+    private val latencyTracker = mock<LatencyTracker>()
 
     private val deviceStateManager = kosmos.deviceStateManager
     private val closedDeviceState = kosmos.foldedDeviceStateList.first()
@@ -142,6 +152,7 @@
                 displaySwitchLatencyLogger,
                 systemClock,
                 deviceStateManager,
+                latencyTracker,
             )
     }
 
@@ -195,6 +206,7 @@
                     displaySwitchLatencyLogger,
                     systemClock,
                     deviceStateManager,
+                    latencyTracker,
                 )
 
             displaySwitchLatencyTracker.start()
@@ -370,6 +382,283 @@
         }
     }
 
+    @Test
+    fun unfoldingDevice_startsLatencyTracking() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+
+            verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun foldingDevice_doesntTrackLatency() {
+        testScope.runTest {
+            setDeviceState(UNFOLDED)
+            displaySwitchLatencyTracker.start()
+            runCurrent()
+
+            startFolding()
+
+            verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun foldedState_doesntStartTrackingOnScreenOn() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            unfoldTransitionProgressProvider.onTransitionStarted()
+            runCurrent()
+
+            verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() {
+        testScope.runTest {
+            animationStatusRepository.onAnimationStatusChanged(enabled = false)
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() {
+        testScope.runTest {
+            animationStatusRepository.onAnimationStatusChanged(enabled = false)
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON)
+            runCurrent()
+
+            verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            finishFolding()
+
+            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            startUnfolding()
+            startFolding()
+            startUnfolding()
+            finishUnfolding()
+
+            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            startUnfolding()
+            startFolding()
+            startUnfolding()
+
+            verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            finishFolding()
+
+            advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds))
+            startUnfolding()
+            finishUnfolding()
+
+            verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            finishFolding()
+
+            advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds))
+            startUnfolding()
+            finishUnfolding()
+
+            verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+            verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchInterrupted_coolDownExtendedByStartEvents() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds))
+            startUnfolding()
+            advanceTimeBy(20.milliseconds)
+
+            startFolding()
+            finishUnfolding()
+
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            startFolding()
+            startUnfolding()
+            advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds)
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            advanceTimeBy(20.milliseconds)
+
+            startFolding()
+            finishUnfolding()
+
+            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun displaySwitchTimedOut_trackingCancelled() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+
+            startUnfolding()
+            advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds)
+            finishUnfolding()
+
+            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+        }
+    }
+
+    @Test
+    fun foldingStarted_screenStillOn_eventSentOnlyAfterScreenSwitches() {
+        // can happen for both folding and unfolding (with animations off) but it's more likely to
+        // happen when folding as waiting for screen on is the default case then
+        testScope.runTest {
+            startInUnfoldedState(displaySwitchLatencyTracker)
+            setDeviceState(FOLDED)
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            verifyNoMoreInteractions(displaySwitchLatencyLogger)
+
+            powerInteractor.setScreenPowerState(SCREEN_OFF)
+            runCurrent()
+            powerInteractor.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            verify(displaySwitchLatencyLogger).log(any())
+        }
+    }
+
+    private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) {
+        setDeviceState(FOLDED)
+        tracker.start()
+        runCurrent()
+    }
+
+    private suspend fun TestScope.startInUnfoldedState(tracker: DisplaySwitchLatencyTracker) {
+        setDeviceState(UNFOLDED)
+        tracker.start()
+        runCurrent()
+    }
+
+    private suspend fun TestScope.startUnfolding() {
+        setDeviceState(HALF_FOLDED)
+        powerInteractor.setScreenPowerState(SCREEN_OFF)
+        runCurrent()
+    }
+
+    private suspend fun TestScope.startFolding() {
+        setDeviceState(FOLDED)
+        powerInteractor.setScreenPowerState(SCREEN_OFF)
+        runCurrent()
+    }
+
+    private fun TestScope.finishFolding() {
+        powerInteractor.setScreenPowerState(SCREEN_ON)
+        runCurrent()
+    }
+
+    private fun TestScope.finishUnfolding() {
+        unfoldTransitionProgressProvider.onTransitionStarted()
+        runCurrent()
+    }
+
     private suspend fun setDeviceState(state: DeviceState) {
         foldStateRepository.emit(state)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 75f3386..b8e1924 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -47,6 +47,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.TestScopeProvider;
+import com.android.settingslib.volume.MediaSessions;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestCaseExtKt;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -268,13 +269,15 @@
     @Test
     public void testOnRemoteVolumeChanged_newStream_noNullPointer() {
         MediaSession.Token token = new MediaSession.Token(Process.myUid(), null);
-        mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(token, 0);
+        var sessionId = MediaSessions.SessionId.Companion.from(token);
+        mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(sessionId, 0);
     }
 
     @Test
     public void testOnRemoteRemove_newStream_noNullPointer() {
         MediaSession.Token token = new MediaSession.Token(Process.myUid(), null);
-        mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(token);
+        var sessionId = MediaSessions.SessionId.Companion.from(token);
+        mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(sessionId);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
index 61ee5e0..390518f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.window.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
@@ -32,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP)
 class WindowRootViewModelTest : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
diff --git a/packages/SystemUI/plugin_core/proguard.flags b/packages/SystemUI/plugin_core/proguard.flags
index 6240898..8b78ba4 100644
--- a/packages/SystemUI/plugin_core/proguard.flags
+++ b/packages/SystemUI/plugin_core/proguard.flags
@@ -8,4 +8,7 @@
 -keep interface com.android.systemui.plugins.annotations.** {
     *;
 }
--keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification @com.android.systemui.plugins.annotations.** class *
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification @com.android.systemui.plugins.annotations.** class * {
+    void <init>();
+}
diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags
index e152c98..2b908a7 100644
--- a/packages/SystemUI/proguard_common.flags
+++ b/packages/SystemUI/proguard_common.flags
@@ -12,8 +12,14 @@
 # Note that we restrict this to SysUISingleton classes, as other registering
 # classes should either *always* unregister or *never* register from their
 # constructor. We also keep callback class names for easier debugging.
--keepnames @com.android.systemui.util.annotations.WeaklyReferencedCallback class *
--keepnames class * extends @com.android.systemui.util.annotations.WeaklyReferencedCallback **
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keepnames @com.android.systemui.util.annotations.WeaklyReferencedCallback class * {
+  void <init>();
+}
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keepnames class * extends @com.android.systemui.util.annotations.WeaklyReferencedCallback ** {
+  void <init>();
+}
 -if @com.android.systemui.util.annotations.WeaklyReferencedCallback class *
 -keepclassmembers,allowaccessmodification @com.android.systemui.dagger.SysUISingleton class * {
   <1> *;
@@ -23,10 +29,16 @@
   <1> *;
 }
 
--keep class androidx.core.app.CoreComponentFactory
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keep class androidx.core.app.CoreComponentFactory {
+  void <init>();
+}
 
 # Keep the wm shell lib
--keep class com.android.wm.shell.*
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keep class com.android.wm.shell.* {
+  void <init>();
+}
 # Keep the protolog group methods that are called by the generated code
 -keepclassmembers class com.android.wm.shell.protolog.ShellProtoLogGroup {
     *;
diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml
new file mode 100644
index 0000000..1ba637f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/clipboard_minimized_background"
+    android:inset="4dp"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 91cd019..43808f2 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -149,9 +149,9 @@
         style="@style/TextAppearance.AuthCredential.Indicator"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:layout_marginTop="24dp"
         android:layout_marginHorizontal="24dp"
-        android:accessibilityLiveRegion="assertive"
+        android:layout_marginTop="24dp"
+        android:accessibilityLiveRegion="polite"
         android:fadingEdge="horizontal"
         android:gravity="center_horizontal"
         android:scrollHorizontally="true"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 448b3e7..915563b 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -171,12 +171,12 @@
         android:layout_height="wrap_content"
         android:visibility="gone"
         android:elevation="7dp"
-        android:padding="8dp"
+        android:padding="12dp"
         app:layout_constraintBottom_toTopOf="@id/indication_container"
         app:layout_constraintStart_toStartOf="parent"
-        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
-        android:background="@drawable/clipboard_minimized_background">
+        android:layout_marginStart="4dp"
+        android:layout_marginBottom="2dp"
+        android:background="@drawable/clipboard_minimized_background_inset">
         <ImageView
             android:src="@drawable/ic_content_paste"
             android:tint="?attr/overlayButtonTextColor"
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index b9ef88e..32407c6 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -147,6 +147,7 @@
                 android:id="@+id/batteryRemainingIcon"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:visibility="gone"
                 app:textAppearance="@style/TextAppearance.QS.Status" />
         </LinearLayout>
     </FrameLayout>
diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml
index 6f42286..b66a88a 100644
--- a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml
+++ b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml
@@ -43,9 +43,6 @@
              ongoing_activity_chip_short_time_delta] will ever be shown at one time. -->
 
         <!-- Shows a timer, like 00:01. -->
-        <!-- Don't use the LimitedWidth style for the timer because the end of the timer is often
-             the most important value. ChipChronometer has the correct logic for when the timer is
-             too large for the space allowed. -->
         <com.android.systemui.statusbar.chips.ui.view.ChipChronometer
             android:id="@+id/ongoing_activity_chip_time"
             style="@style/StatusBar.Chip.Text"
@@ -54,14 +51,14 @@
         <!-- Shows generic text. -->
         <com.android.systemui.statusbar.chips.ui.view.ChipTextView
             android:id="@+id/ongoing_activity_chip_text"
-            style="@style/StatusBar.Chip.Text.LimitedWidth"
+            style="@style/StatusBar.Chip.Text"
             android:visibility="gone"
             />
 
         <!-- Shows a time delta in short form, like "15min" or "1hr". -->
         <com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView
             android:id="@+id/ongoing_activity_chip_short_time_delta"
-            style="@style/StatusBar.Chip.Text.LimitedWidth"
+            style="@style/StatusBar.Chip.Text"
             android:visibility="gone"
             />
 
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index c28dc50..bb99d58 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -34,12 +34,17 @@
         android:orientation="horizontal"/>
 
     <!-- PaddingEnd is added to balance hover padding, compensating for paddingStart in statusIcons.
-         See b/339589733 -->
+         See b/339589733.
+
+         Default visibility is now "gone" to make space for the new battery icon
+         -->
     <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:clipToPadding="false"
         android:clipChildren="false"
         android:paddingEnd="@dimen/status_bar_battery_end_padding"
+        android:visibility="gone"
         systemui:textAppearance="@style/TextAppearance.StatusBar.Clock" />
+
 </LinearLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 67f620f..8ad99ab 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -16,7 +16,7 @@
 <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/volume_dialog_root"
+    android:id="@+id/volume_dialog"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:alpha="0"
diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml
index 6748cfa..4e3c8cc 100644
--- a/packages/SystemUI/res/layout/volume_ringer_button.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_button.xml
@@ -13,20 +13,13 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content" >
-
-    <ImageButton
-        android:id="@+id/volume_drawer_button"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
-        android:contentDescription="@string/volume_ringer_mode"
-        android:gravity="center"
-        android:tint="@androidprv:color/materialColorOnSurface"
-        android:src="@drawable/volume_ringer_item_bg"
-        android:background="@drawable/volume_ringer_item_bg"/>
-
-</FrameLayout>
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/volume_ringer_item_bg"
+    android:contentDescription="@string/volume_ringer_mode"
+    android:gravity="center"
+    android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
+    android:src="@drawable/volume_ringer_item_bg"
+    android:tint="@androidprv:color/materialColorOnSurface" />
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9b8926e..09aa224 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1110,4 +1110,7 @@
     <!-- Configuration for wallpaper focal area -->
     <bool name="center_align_focal_area_shape">false</bool>
     <string name="focal_area_target" translatable="false" />
+
+    <!-- Configuration to swipe to open glanceable hub -->
+    <bool name="config_swipeToOpenGlanceableHub">false</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2d3c07b..648e4c2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1811,6 +1811,7 @@
     <dimen name="ongoing_activity_chip_text_end_padding_for_embedded_padding_icon">6dp</dimen>
     <dimen name="ongoing_activity_chip_text_fading_edge_length">12dp</dimen>
     <dimen name="ongoing_activity_chip_corner_radius">28dp</dimen>
+    <dimen name="ongoing_activity_chip_outline_width">2px</dimen>
 
     <!-- Status bar user chip -->
     <dimen name="status_bar_user_chip_avatar_size">16dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d18a90a..8629203 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1351,6 +1351,10 @@
     <string name="accessibility_action_label_shrink_widget">Decrease height</string>
     <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_label_expand_widget">Increase height</string>
+    <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_umo_show_next">Show next</string>
+    <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_umo_show_previous">Show previous</string>
     <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
     <string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
     <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 5ef4d40..4961a7e 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -93,15 +93,6 @@
         <item name="android:textColor">?android:attr/colorPrimary</item>
     </style>
 
-    <!-- Style for a status bar chip text that has a maximum width. Since there's so little room in
-         the status bar chip area, don't ellipsize the text and instead just fade it out a bit at
-         the end. -->
-    <style name="StatusBar.Chip.Text.LimitedWidth">
-        <item name="android:ellipsize">none</item>
-        <item name="android:requiresFadingEdge">horizontal</item>
-        <item name="android:fadingEdgeLength">@dimen/ongoing_activity_chip_text_fading_edge_length</item>
-    </style>
-
     <style name="Chipbar" />
 
     <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title">
@@ -258,7 +249,7 @@
     <style name="TextAppearance.AuthNonBioCredential.Title">
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:layout_marginTop">24dp</item>
-        <item name="android:textSize">36dp</item>
+        <item name="android:textSize">36sp</item>
         <item name="android:focusable">true</item>
         <item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
     </style>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 335a910..73dc282 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -98,7 +98,6 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.settingslib.Utils;
 import com.android.settingslib.drawable.CircleFramedDrawable;
-import com.android.systemui.Flags;
 import com.android.systemui.FontStyles;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.classifier.FalsingA11yDelegate;
@@ -121,6 +120,7 @@
     static final int USER_TYPE_PRIMARY = 1;
     static final int USER_TYPE_WORK_PROFILE = 2;
     static final int USER_TYPE_SECONDARY_USER = 3;
+    private boolean mTransparentModeEnabled = false;
 
     @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
     public @interface Mode {}
@@ -814,15 +814,30 @@
         mDisappearAnimRunning = false;
     }
 
+    /**
+     * Make the bouncer background transparent
+     */
+    public void enableTransparentMode() {
+        mTransparentModeEnabled = true;
+        reloadBackgroundColor();
+    }
+
+    /**
+     * Make the bouncer background opaque
+     */
+    public void disableTransparentMode() {
+        mTransparentModeEnabled = false;
+        reloadBackgroundColor();
+    }
+
     private void reloadBackgroundColor() {
-        if (Flags.bouncerUiRevamp()) {
-            // Keep the background transparent, otherwise the background color looks like a box
-            // while scaling the bouncer for back animation or while transitioning to the bouncer.
+        if (mTransparentModeEnabled) {
             setBackgroundColor(Color.TRANSPARENT);
         } else {
             setBackgroundColor(
                     getContext().getColor(com.android.internal.R.color.materialColorSurfaceDim));
         }
+        invalidate();
     }
 
     void reloadColors() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index ff7b2b0..d10fce4 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -70,6 +70,7 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.keyguard.dagger.KeyguardBouncerScope;
 import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.Flags;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
@@ -96,6 +97,8 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.GlobalSettings;
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepository;
+import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor;
 
 import dagger.Lazy;
 
@@ -134,6 +137,7 @@
     private final FalsingA11yDelegate mFalsingA11yDelegate;
     private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor;
     private final BouncerMessageInteractor mBouncerMessageInteractor;
+    private final Lazy<WindowRootViewBlurInteractor> mRootViewBlurInteractor;
     private int mTranslationY;
     private final KeyguardDismissTransitionInteractor mKeyguardDismissTransitionInteractor;
     private final DevicePolicyManager mDevicePolicyManager;
@@ -431,6 +435,7 @@
     private final Executor mBgExecutor;
     @Nullable
     private Job mSceneTransitionCollectionJob;
+    private Job mBlurEnabledCollectionJob;
 
     @Inject
     public KeyguardSecurityContainerController(KeyguardSecurityContainer view,
@@ -463,9 +468,11 @@
             KeyguardDismissTransitionInteractor keyguardDismissTransitionInteractor,
             Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
             @Background Executor bgExecutor,
-            Provider<DeviceEntryInteractor> deviceEntryInteractor
+            Provider<DeviceEntryInteractor> deviceEntryInteractor,
+            Lazy<WindowRootViewBlurInteractor> rootViewBlurInteractorProvider
     ) {
         super(view);
+        mRootViewBlurInteractor = rootViewBlurInteractorProvider;
         view.setAccessibilityDelegate(faceAuthAccessibilityDelegate);
         mLockPatternUtils = lockPatternUtils;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -539,6 +546,32 @@
                 }
             );
         }
+
+        if (Flags.bouncerUiRevamp()) {
+            mBlurEnabledCollectionJob = mJavaAdapter.get().alwaysCollectFlow(
+                    mRootViewBlurInteractor.get().isBlurCurrentlySupported(),
+                    this::handleBlurSupportedChanged);
+        }
+    }
+
+    private void handleBlurSupportedChanged(boolean isWindowBlurSupported) {
+        if (isWindowBlurSupported) {
+            mView.enableTransparentMode();
+        } else {
+            mView.disableTransparentMode();
+        }
+    }
+
+    private void refreshBouncerBackground() {
+        // This is present solely for screenshot tests that disable blur by invoking setprop to
+        // disable blurs, however the mRootViewBlurInteractor#isBlurCurrentlySupported doesn't emit
+        // an updated value because sysui doesn't have a way to register for changes to setprop.
+        // KeyguardSecurityContainer view is inflated only once and doesn't re-inflate so it has to
+        // check the sysprop every time bouncer is about to be shown.
+        if (Flags.bouncerUiRevamp() && (ActivityManager.isRunningInUserTestHarness()
+                || ActivityManager.isRunningInTestHarness())) {
+            handleBlurSupportedChanged(!WindowRootViewBlurRepository.isDisableBlurSysPropSet());
+        }
     }
 
     @Override
@@ -552,6 +585,11 @@
             mSceneTransitionCollectionJob.cancel(null);
             mSceneTransitionCollectionJob = null;
         }
+
+        if (mBlurEnabledCollectionJob != null) {
+            mBlurEnabledCollectionJob.cancel(null);
+            mBlurEnabledCollectionJob = null;
+        }
     }
 
     /**  */
@@ -718,6 +756,8 @@
         if (bouncerUserSwitcher != null) {
             bouncerUserSwitcher.setAlpha(0f);
         }
+
+        refreshBouncerBackground();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index f835ad6..e2065f1 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -352,6 +352,7 @@
                             && Math.abs(delta) > Math.abs(deltaPerpendicular)) {
                         if (mCallback.canChildBeDragged(mTouchedView)) {
                             mIsSwiping = true;
+                            mCallback.setMagneticAndRoundableTargets(mTouchedView);
                             mCallback.onBeginDrag(mTouchedView);
                             mInitialTouchPos = getPos(ev);
                             mTranslation = getTranslation(mTouchedView);
@@ -444,6 +445,7 @@
         };
 
         Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
+        mCallback.onMagneticInteractionEnd(animView, velocity);
         if (anim == null) {
             onDismissChildWithAnimationFinished();
             return;
@@ -733,7 +735,8 @@
                         dismissChild(mTouchedView, velocity,
                                 !swipedFastEnough() /* useAccelerateInterpolator */);
                     } else {
-                        mCallback.onDragCancelledWithVelocity(mTouchedView, velocity);
+                        mCallback.onMagneticInteractionEnd(mTouchedView, velocity);
+                        mCallback.onDragCancelled(mTouchedView);
                         snapChild(mTouchedView, 0 /* leftTarget */, velocity);
                     }
                     mTouchedView = null;
@@ -935,18 +938,24 @@
 
         void onBeginDrag(View v);
 
+        /**
+         * Set magnetic and roundable targets for a view.
+         */
+        void setMagneticAndRoundableTargets(View v);
+
         void onChildDismissed(View v);
 
         void onDragCancelled(View v);
 
         /**
-         * A drag operation has been cancelled on a view with a final velocity.
-         * @param v View that was dragged.
-         * @param finalVelocity Final velocity of the drag.
+         * Notify that a magnetic interaction ended on a view with a velocity.
+         * <p>
+         * This method should be called when a view will snap back or be dismissed.
+         *
+         * @param view The {@link  View} whose magnetic interaction ended.
+         * @param velocity The velocity when the interaction ended.
          */
-        default void onDragCancelledWithVelocity(View v, float finalVelocity) {
-            onDragCancelled(v);
-        }
+        void onMagneticInteractionEnd(View view, float velocity);
 
         /**
          * Called when the child is long pressed and available to start drag and drop.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
index 1ed8c06..5a59b7a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 include /core/java/android/view/accessibility/OWNERS
 jonesriley@google.com
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index a67ec65..8734d05 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -296,6 +296,7 @@
         mGestureDetector =
                 new MagnificationGestureDetector(mContext, handler, this);
         mWindowInsetChangeRunnable = this::onWindowInsetChanged;
+        mWindowInsetChangeRunnable.run();
 
         // Initialize listeners.
         mMirrorViewRunnable = new Runnable() {
@@ -367,8 +368,12 @@
     private boolean updateSystemGestureInsetsTop() {
         final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics();
         final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures());
-        final int gestureTop =
-                insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1;
+        final int gestureTop;
+        if (Flags.updateWindowMagnifierBottomBoundary()) {
+            gestureTop = windowMetrics.getBounds().bottom - insets.bottom;
+        } else {
+            gestureTop = insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1;
+        }
         if (gestureTop != mSystemGestureTop) {
             mSystemGestureTop = gestureTop;
             return true;
@@ -953,7 +958,6 @@
                 ? mSystemGestureTop - height + mOuterBorderSize
                 : mWindowBounds.bottom - height + mOuterBorderSize;
         final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY);
-
         if (computeWindowSize) {
             LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
             params.width = width;
diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
index b65d29c..429b4b0 100644
--- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
@@ -5,5 +5,4 @@
 pauldpong@google.com
 praveenj@google.com
 vicliang@google.com
-mfolkerts@google.com
 yuklimko@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index e365b77..d8e7a16 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.wm.shell.animation.FlingAnimationUtils
@@ -79,6 +80,7 @@
     private val activityStarter: ActivityStarter,
     private val keyguardInteractor: KeyguardInteractor,
     private val sceneInteractor: SceneInteractor,
+    private val shadeRepository: ShadeRepository,
     private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
 ) : TouchHandler {
     /** An interface for creating ValueAnimators. */
@@ -260,6 +262,8 @@
         }
         scrimManager.addCallback(scrimManagerCallback)
         currentScrimController = scrimManager.currentController
+
+        shadeRepository.setLegacyShadeTracking(true)
         session.registerCallback {
             velocityTracker?.apply { recycle() }
             velocityTracker = null
@@ -270,6 +274,7 @@
             if (!Flags.communalBouncerDoNotModifyPluginOpen()) {
                 notificationShadeWindowController.setForcePluginOpen(false, this)
             }
+            shadeRepository.setLegacyShadeTracking(false)
         }
         session.registerGestureListener(onGestureListener)
         session.registerInputListener { ev: InputEvent -> onMotionEvent(ev) }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index 9a30c21..fcf5105 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -46,7 +46,10 @@
 
 import javax.inject.Inject;
 
-/** Controller for {@link BatteryMeterView}. **/
+/**
+ * Controller for {@link BatteryMeterView}.
+ * @deprecated once [NewStatusBarIcons] is rolled out, this class is no longer needed
+ */
 public class BatteryMeterViewController extends ViewController<BatteryMeterView> {
     private final ConfigurationController mConfigurationController;
     private final TunerService mTunerService;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 6cd763a..bbf9a1901 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -31,6 +31,7 @@
 import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieProperty
 import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.keyguard.KeyguardPINView
 import com.android.systemui.CoreStartable
 import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
@@ -50,7 +51,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
 @SysUISingleton
@@ -65,51 +65,53 @@
     private val layoutInflater: Lazy<LayoutInflater>,
     private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
     private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
-    private val windowManager: Lazy<WindowManager>
+    private val windowManager: Lazy<WindowManager>,
 ) : CoreStartable {
 
     override fun start() {
-        applicationScope
-            .launch {
-                sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
-                    if (isSfpsAvailable) {
-                        combine(
-                                biometricStatusInteractor.get().sfpsAuthenticationReason,
-                                deviceEntrySideFpsOverlayInteractor
-                                    .get()
-                                    .showIndicatorForDeviceEntry,
-                                sideFpsProgressBarViewModel.get().isVisible,
-                                ::Triple
+        applicationScope.launch {
+            sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
+                if (isSfpsAvailable) {
+                    combine(
+                            biometricStatusInteractor.get().sfpsAuthenticationReason,
+                            deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry,
+                            sideFpsProgressBarViewModel.get().isVisible,
+                            ::Triple,
+                        )
+                        .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
+                        .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+                            val (
+                                systemServerAuthReason,
+                                showIndicatorForDeviceEntry,
+                                progressBarIsVisible) =
+                                combinedFlows
+                            Log.d(
+                                TAG,
+                                "systemServerAuthReason = $systemServerAuthReason, " +
+                                    "showIndicatorForDeviceEntry = " +
+                                    "$showIndicatorForDeviceEntry, " +
+                                    "progressBarIsVisible = $progressBarIsVisible",
                             )
-                            .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
-                            .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
-                                val (
-                                    systemServerAuthReason,
-                                    showIndicatorForDeviceEntry,
-                                    progressBarIsVisible) =
-                                    combinedFlows
-                                Log.d(
-                                    TAG,
-                                    "systemServerAuthReason = $systemServerAuthReason, " +
-                                        "showIndicatorForDeviceEntry = " +
-                                        "$showIndicatorForDeviceEntry, " +
-                                        "progressBarIsVisible = $progressBarIsVisible"
-                                )
-                                if (!isInRearDisplayMode) {
-                                    if (progressBarIsVisible) {
-                                        hide()
-                                    } else if (systemServerAuthReason != NotRunning) {
-                                        show()
-                                    } else if (showIndicatorForDeviceEntry) {
-                                        show()
-                                    } else {
-                                        hide()
-                                    }
+                            if (!isInRearDisplayMode) {
+                                if (progressBarIsVisible) {
+                                    hide()
+                                } else if (systemServerAuthReason != NotRunning) {
+                                    show()
+                                } else if (showIndicatorForDeviceEntry) {
+                                    show()
+                                    overlayView?.announceForAccessibility(
+                                        applicationContext.resources.getString(
+                                            R.string.accessibility_side_fingerprint_indicator_label
+                                        )
+                                    )
+                                } else {
+                                    hide()
                                 }
                             }
-                    }
+                        }
                 }
             }
+        }
     }
 
     private var overlayView: View? = null
@@ -119,7 +121,7 @@
         if (overlayView?.isAttachedToWindow == true) {
             Log.d(
                 TAG,
-                "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request"
+                "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request",
             )
             return
         }
@@ -137,11 +139,6 @@
         overlayView!!.visibility = View.INVISIBLE
         Log.d(TAG, "show(): adding overlayView $overlayView")
         windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
-        overlayView!!.announceForAccessibility(
-            applicationContext.resources.getString(
-                R.string.accessibility_side_fingerprint_indicator_label
-            )
-        )
     }
 
     /** Hide the side fingerprint sensor indicator */
@@ -163,7 +160,7 @@
         fun bind(
             overlayView: View,
             viewModel: SideFpsOverlayViewModel,
-            windowManager: WindowManager
+            windowManager: WindowManager,
         ) {
             overlayView.repeatWhenAttached {
                 val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
@@ -186,7 +183,7 @@
                     object : View.AccessibilityDelegate() {
                         override fun dispatchPopulateAccessibilityEvent(
                             host: View,
-                            event: AccessibilityEvent
+                            event: AccessibilityEvent,
                         ): Boolean {
                             return if (
                                 event.getEventType() ===
diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
index 2e1b5ad..e456310 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
@@ -17,16 +17,19 @@
 package com.android.systemui.communal;
 
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
-import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.shared.model.DozeStateModel;
 import com.android.systemui.shared.condition.Condition;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
 
 import javax.inject.Inject;
 
@@ -38,6 +41,10 @@
     private final KeyguardStateController mKeyguardStateController;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final KeyguardInteractor mKeyguardInteractor;
+    private final JavaAdapter mJavaAdapter;
+    private Job mAnyDozeListenerJob;
+    private boolean mAnyDoze;
     private final KeyguardStateController.Callback mKeyguardStateCallback =
             new KeyguardStateController.Callback() {
                 @Override
@@ -63,12 +70,14 @@
     @Inject
     public DeviceInactiveCondition(@Application CoroutineScope scope,
             KeyguardStateController keyguardStateController,
-            WakefulnessLifecycle wakefulnessLifecycle,
-            KeyguardUpdateMonitor keyguardUpdateMonitor) {
+            WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor,
+            KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) {
         super(scope);
         mKeyguardStateController = keyguardStateController;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mKeyguardInteractor = keyguardInteractor;
+        mJavaAdapter = javaAdapter;
     }
 
     @Override
@@ -77,6 +86,11 @@
         mKeyguardStateController.addCallback(mKeyguardStateCallback);
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
         mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+        mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow(
+                mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> {
+                    mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo());
+                    updateState();
+                });
     }
 
     @Override
@@ -84,6 +98,7 @@
         mKeyguardStateController.removeCallback(mKeyguardStateCallback);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
         mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
+        mAnyDozeListenerJob.cancel(null);
     }
 
     @Override
@@ -92,10 +107,10 @@
     }
 
     private void updateState() {
-        final boolean asleep =
-                mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
-                        || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP;
-        updateCondition(asleep || mKeyguardStateController.isShowing()
-                || mKeyguardUpdateMonitor.isDreaming());
+        final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP;
+        // Doze/AoD is also a dream, but we should never override it with low light as to the user
+        // it's totally unrelated.
+        updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing()
+                || mKeyguardUpdateMonitor.isDreaming()));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index f01a6db..ff74162 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -104,6 +104,7 @@
     companion object {
         const val LOGGABLE_PREFIXES = "loggable_prefixes"
         const val LAUNCHER_PACKAGE = "launcher_package"
+        const val SWIPE_TO_HUB = "swipe_to_hub"
 
         @Provides
         @Communal
@@ -143,5 +144,11 @@
         fun provideLauncherPackage(@Main resources: Resources): String {
             return resources.getString(R.string.launcher_overlayable_package)
         }
+
+        @Provides
+        @Named(SWIPE_TO_HUB)
+        fun provideSwipeToHub(@Main resources: Resources): Boolean {
+            return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 49003a7..a4860df 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -202,6 +202,12 @@
     /** Called as the user request to show the customize widget button. */
     open fun onLongClick() {}
 
+    /** Called as the user requests to switch to the previous player in UMO. */
+    open fun onShowPreviousMedia() {}
+
+    /** Called as the user requests to switch to the next player in UMO. */
+    open fun onShowNextMedia() {}
+
     /** Called as the UI determines that a new widget has been added to the grid. */
     open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 4bc4400..2169881 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
@@ -92,6 +93,7 @@
     private val metricsLogger: CommunalMetricsLogger,
     mediaCarouselController: MediaCarouselController,
     blurConfig: BlurConfig,
+    @Named(SWIPE_TO_HUB) private val swipeToHub: Boolean,
 ) :
     BaseCommunalViewModel(
         communalSceneInteractor,
@@ -254,6 +256,14 @@
         }
     }
 
+    override fun onShowPreviousMedia() {
+        mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1)
+    }
+
+    override fun onShowNextMedia() {
+        mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1)
+    }
+
     override fun onTapWidget(componentName: ComponentName, rank: Int) {
         metricsLogger.logTapWidget(componentName.flattenToString(), rank)
     }
@@ -349,6 +359,8 @@
     /** See [CommunalSettingsInteractor.isV2FlagEnabled] */
     fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled()
 
+    fun swipeToHubEnabled(): Boolean = swipeToHub
+
     companion object {
         const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 3c68e3a..8bff090 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -74,6 +74,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
 import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule;
+import com.android.systemui.statusbar.notification.dagger.NotificationStackModule;
 import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpModule;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -169,6 +170,7 @@
         WallpaperModule.class,
         ShortcutHelperModule.class,
         ContextualEducationModule.class,
+        NotificationStackModule.class,
 })
 public abstract class ReferenceSystemUIModule {
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index fcc3ea9..fed7709 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -18,6 +18,7 @@
 
 import com.android.keyguard.KeyguardBiometricLockoutLogger
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
 import com.android.systemui.LatencyTester
 import com.android.systemui.SliceBroadcastRelayHandler
 import com.android.systemui.accessibility.Magnification
@@ -60,6 +61,7 @@
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.theme.ThemeOverlayController
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker
+import com.android.systemui.unfold.NoCooldownDisplaySwitchLatencyTracker
 import com.android.systemui.usb.StorageNotification
 import com.android.systemui.util.NotificationChannels
 import com.android.systemui.util.StartBinderLoggerModule
@@ -67,8 +69,10 @@
 import com.android.systemui.wmshell.WMShell
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
+import javax.inject.Provider
 
 /**
  * DEPRECATED: DO NOT ADD THINGS TO THIS FILE.
@@ -148,12 +152,6 @@
     @ClassKey(LatencyTester::class)
     abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable
 
-    /** Inject into DisplaySwitchLatencyTracker. */
-    @Binds
-    @IntoMap
-    @ClassKey(DisplaySwitchLatencyTracker::class)
-    abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable
-
     /** Inject into NotificationChannels. */
     @Binds
     @IntoMap
@@ -353,4 +351,15 @@
     @IntoMap
     @ClassKey(ComplicationTypesUpdater::class)
     abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable
+
+    companion object {
+        @Provides
+        @IntoMap
+        @ClassKey(DisplaySwitchLatencyTracker::class)
+        fun provideDisplaySwitchLatencyTracker(
+            noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>,
+            coolDownVariant: Provider<DisplaySwitchLatencyTracker>,
+        ): CoreStartable =
+            if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
index 9607053..b712fde 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
@@ -39,6 +39,8 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flow
@@ -99,6 +101,25 @@
         waitForDeviceConnection(deviceType)
     }
 
+    // This flow is used by the notification updater once an initial notification is launched. It
+    // listens to the device connection changes for both keyboard and touchpad. When either of the
+    // device is disconnected, resolve the tutorial type base on the latest connection state.
+    // Dropping the initial state because it's the existing notification. Filtering out BOTH because
+    // we only care about disconnections.
+    val tutorialTypeUpdates: Flow<TutorialType> =
+        keyboardRepository.isAnyKeyboardConnected
+            .combine(touchpadRepository.isAnyTouchpadConnected, ::Pair)
+            .map { (keyboardConnected, touchpadConnected) ->
+                when {
+                    keyboardConnected && touchpadConnected -> TutorialType.BOTH
+                    keyboardConnected -> TutorialType.KEYBOARD
+                    touchpadConnected -> TutorialType.TOUCHPAD
+                    else -> TutorialType.NONE
+                }
+            }
+            .drop(1)
+            .filter { it != TutorialType.BOTH }
+
     private suspend fun waitForDeviceConnection(deviceType: DeviceType) =
         isAnyDeviceConnected[deviceType]!!.filter { it }.first()
 
@@ -172,6 +193,7 @@
                         pw.println(
                             "         launch time = ${repo.getScheduledTutorialLaunchTime(TOUCHPAD)}"
                         )
+                        pw.println("Delay time = ${LAUNCH_DELAY.seconds} sec")
                     }
                 "notify" -> {
                     if (args.size != 2) help(pw)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
index 3cba70e..a90c7ad 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
@@ -42,6 +42,9 @@
 import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.merge
 
 /** When the scheduler is due, show a notification to launch tutorial */
@@ -55,19 +58,43 @@
     private val notificationManager: NotificationManager,
     private val userTracker: UserTracker,
 ) {
+    private var updaterJob: Job? = null
+
     fun start() {
         backgroundScope.launch {
             merge(
                     tutorialSchedulerInteractor.tutorials,
                     tutorialSchedulerInteractor.commandTutorials,
                 )
-                .collect { showNotification(it) }
+                .filter { it != TutorialType.NONE }
+                .collectLatest {
+                    showNotification(it)
+                    updaterJob?.cancel()
+                    updaterJob = backgroundScope.launch { updateWhenDeviceDisconnects() }
+                }
         }
     }
 
+    private suspend fun updateWhenDeviceDisconnects() {
+        // Only update the notification when there is an active one (i.e. if the notification has
+        // been dismissed by the user, or if the tutorial has been launched, there's no need to
+        // update)
+        tutorialSchedulerInteractor.tutorialTypeUpdates
+            .filter { hasNotification() }
+            .collect {
+                if (it == TutorialType.NONE)
+                    notificationManager.cancelAsUser(TAG, NOTIFICATION_ID, userTracker.userHandle)
+                else showNotification(it)
+            }
+    }
+
+    private fun hasNotification() =
+        notificationManager.activeNotifications.any { it.id == NOTIFICATION_ID }
+
     // By sharing the same tag and id, we update the content of existing notification instead of
     // creating multiple notifications
     private fun showNotification(tutorialType: TutorialType) {
+        // Safe guard - but this should never been reached
         if (tutorialType == TutorialType.NONE) return
 
         if (notificationManager.getNotificationChannel(CHANNEL_ID) == null)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
index fdb80b2..978b873 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
@@ -52,17 +52,15 @@
         val shortcuts = mutableListOf<KeyboardShortcutInfo>()
 
         if (keyboardA11yShortcutControl()) {
-            if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
-                shortcuts.add(
-                    // Toggle bounce keys:
-                    //  - Meta + Alt + 3
-                    shortcutInfo(
-                        resources.getString(R.string.group_accessibility_toggle_bounce_keys)
-                    ) {
-                        command(META_META_ON or META_ALT_ON, KEYCODE_3)
-                    }
-                )
-            }
+            shortcuts.add(
+                // Toggle bounce keys:
+                //  - Meta + Alt + 3
+                shortcutInfo(
+                    resources.getString(R.string.group_accessibility_toggle_bounce_keys)
+                ) {
+                    command(META_META_ON or META_ALT_ON, KEYCODE_3)
+                }
+            )
             if (InputSettings.isAccessibilityMouseKeysFeatureFlagEnabled()) {
                 shortcuts.add(
                     // Toggle mouse keys:
@@ -74,28 +72,24 @@
                     }
                 )
             }
-            if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
-                shortcuts.add(
-                    // Toggle sticky keys:
-                    //  - Meta + Alt + 5
-                    shortcutInfo(
-                        resources.getString(R.string.group_accessibility_toggle_sticky_keys)
-                    ) {
-                        command(META_META_ON or META_ALT_ON, KEYCODE_5)
-                    }
-                )
-            }
-            if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
-                shortcuts.add(
-                    // Toggle slow keys:
-                    //  - Meta + Alt + 6
-                    shortcutInfo(
-                        resources.getString(R.string.group_accessibility_toggle_slow_keys)
-                    ) {
-                        command(META_META_ON or META_ALT_ON, KEYCODE_6)
-                    }
-                )
-            }
+            shortcuts.add(
+                // Toggle sticky keys:
+                //  - Meta + Alt + 5
+                shortcutInfo(
+                    resources.getString(R.string.group_accessibility_toggle_sticky_keys)
+                ) {
+                    command(META_META_ON or META_ALT_ON, KEYCODE_5)
+                }
+            )
+            shortcuts.add(
+                // Toggle slow keys:
+                //  - Meta + Alt + 6
+                shortcutInfo(
+                    resources.getString(R.string.group_accessibility_toggle_slow_keys)
+                ) {
+                    command(META_META_ON or META_ALT_ON, KEYCODE_6)
+                }
+            )
         }
 
         if (enableVoiceAccessKeyGestures()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index efa9c21..caf0fd4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyguard;
 
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
 import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
 import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED;
@@ -76,7 +75,6 @@
 import android.os.DeadObjectException;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
@@ -194,8 +192,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -286,9 +282,6 @@
     private static final int SYSTEM_READY = 18;
     private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
     private static final int BOOT_INTERACTOR = 20;
-    private static final int BEFORE_USER_SWITCHING = 21;
-    private static final int USER_SWITCHING = 22;
-    private static final int USER_SWITCH_COMPLETE = 23;
 
     /** Enum for reasons behind updating wakeAndUnlock state. */
     @Retention(RetentionPolicy.SOURCE)
@@ -306,8 +299,6 @@
         int WAKE_AND_UNLOCK = 3;
     }
 
-    private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>();
-
     /**
      * The default amount of time we stay awake (used for all key input)
      */
@@ -366,18 +357,13 @@
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
     private final Lazy<ShadeController> mShadeController;
     private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
-    /*
-     * Records the user id on request to go away, for validation when WM calls back to start the
-     * exit animation.
-     */
-    private int mGoingAwayRequestedForUserId = -1;
-
     private boolean mSystemReady;
     private boolean mBootCompleted;
     private boolean mBootSendUserPresent;
     private boolean mShuttingDown;
     private boolean mDozing;
     private boolean mAnimatingScreenOff;
+    private boolean mIgnoreDismiss;
     private final Context mContext;
     private final FalsingCollector mFalsingCollector;
 
@@ -640,78 +626,6 @@
                 }
             };
 
-    @VisibleForTesting
-    protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() {
-
-        @Override
-        public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) {
-            mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING,
-                    newUser, 0, resultCallback));
-        }
-
-        @Override
-        public void onUserChanging(int newUser, @NonNull Context userContext,
-                @NonNull Runnable resultCallback) {
-            mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING,
-                    newUser, 0, resultCallback));
-        }
-
-        @Override
-        public void onUserChanged(int newUser, Context userContext) {
-            mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE,
-                    newUser, 0));
-        }
-    };
-
-    /**
-     * Handle {@link #BEFORE_USER_SWITCHING}
-     */
-    @VisibleForTesting
-    void handleBeforeUserSwitching(int userId, Runnable resultCallback) {
-        Log.d(TAG, String.format("onBeforeUserSwitching %d", userId));
-        synchronized (KeyguardViewMediator.this) {
-            mHandler.removeMessages(DISMISS);
-            notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
-            resetKeyguardDonePendingLocked();
-            adjustStatusBarLocked();
-            mKeyguardStateController.notifyKeyguardGoingAway(false);
-            if (mLockPatternUtils.isSecure(userId) && !mShowing) {
-                doKeyguardLocked(null);
-            } else {
-                resetStateLocked();
-            }
-            resultCallback.run();
-        }
-    }
-
-    /**
-     * Handle {@link #USER_SWITCHING}
-     */
-    @VisibleForTesting
-    void handleUserSwitching(int userId, Runnable resultCallback) {
-        Log.d(TAG, String.format("onUserSwitching %d", userId));
-        synchronized (KeyguardViewMediator.this) {
-            if (!mLockPatternUtils.isSecure(userId)) {
-                dismiss(null, null);
-            }
-            resultCallback.run();
-        }
-    }
-
-    /**
-     * Handle {@link #USER_SWITCH_COMPLETE}
-     */
-    @VisibleForTesting
-    void handleUserSwitchComplete(int userId) {
-        Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
-        // Calling dismiss on a secure user will show the bouncer
-        if (mLockPatternUtils.isSecure(userId)) {
-            // We are calling dismiss with a delay as there are race conditions in some scenarios
-            // caused by async layout listeners
-            mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
-        }
-    }
-
     KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
 
         @Override
@@ -728,6 +642,27 @@
         }
 
         @Override
+        public void onUserSwitching(int userId) {
+            Log.d(TAG, String.format("onUserSwitching %d", userId));
+            synchronized (KeyguardViewMediator.this) {
+                mIgnoreDismiss = true;
+                notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
+                resetKeyguardDonePendingLocked();
+                resetStateLocked();
+                adjustStatusBarLocked();
+            }
+        }
+
+        @Override
+        public void onUserSwitchComplete(int userId) {
+            mIgnoreDismiss = false;
+            Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
+            // We are calling dismiss with a delay as there are race conditions in some scenarios
+            // caused by async layout listeners
+            mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
+        }
+
+        @Override
         public void onDeviceProvisioned() {
             sendUserPresentBroadcast();
         }
@@ -1736,13 +1671,7 @@
                 com.android.internal.R.anim.lock_screen_behind_enter);
 
         mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
-        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
-        // start() can be invoked in the middle of user switching, so check for this state and issue
-        // the call manually as that important event was missed.
-        if (mUserTracker.isUserSwitching()) {
-            handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {});
-            handleUserSwitching(mUserTracker.getUserId(), () -> {});
-        }
+
         mJavaAdapter.alwaysCollectFlow(
                 mWallpaperRepository.getWallpaperSupportsAmbientMode(),
                 this::setWallpaperSupportsAmbientMode);
@@ -1791,7 +1720,7 @@
             // System ready can be invoked in the middle of user switching, so check for this state
             // and issue the call manually as that important event was missed.
             if (mUserTracker.isUserSwitching()) {
-                mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {});
+                mUpdateCallback.onUserSwitching(mUserTracker.getUserId());
             }
         }
         // Most services aren't available until the system reaches the ready state, so we
@@ -2432,23 +2361,12 @@
             mCommunalSceneInteractor.get().showHubFromPowerButton();
         }
 
-        int currentUserId = mSelectedUserInteractor.getSelectedUserId();
-        if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) {
-            LockNowCallback callback = new LockNowCallback(currentUserId,
-                    IRemoteCallback.Stub.asInterface(
-                            options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK)));
-            synchronized (mLockNowCallbacks) {
-                mLockNowCallbacks.add(callback);
-            }
-            Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId);
-        }
-
         // if another app is disabling us, don't show
         if (!mExternallyEnabled
                 && !mLockPatternUtils.isUserInLockdown(
                         mSelectedUserInteractor.getSelectedUserId())) {
             if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
-            notifyLockNowCallback();
+
             mNeedToReshowWhenReenabled = true;
             return;
         }
@@ -2466,7 +2384,6 @@
                     // We're removing "reset" in the refactor - "resetting" the views will happen
                     // as a reaction to the root cause of the "reset" signal.
                     if (KeyguardWmStateRefactor.isEnabled()) {
-                        notifyLockNowCallback();
                         return;
                     }
 
@@ -2479,7 +2396,6 @@
                                     + "previously hiding. It should be safe to short-circuit "
                                     + "here.");
                     resetStateLocked(/* hideBouncer= */ false);
-                    notifyLockNowCallback();
                     return;
                 }
             } else {
@@ -2506,7 +2422,6 @@
                 Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is"
                         + " not locked or missing");
             }
-            notifyLockNowCallback();
             return;
         }
 
@@ -2514,7 +2429,6 @@
         if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId())
                 && !lockedOrMissing && !forceShow) {
             if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off");
-            notifyLockNowCallback();
             return;
         }
 
@@ -2562,6 +2476,11 @@
     }
 
     public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+        if (mIgnoreDismiss) {
+            android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
+            return;
+        }
+
         if (mKeyguardStateController.isKeyguardGoingAway()) {
             Log.i(TAG, "Ignoring dismiss because we're already going away.");
             return;
@@ -2579,7 +2498,7 @@
     }
 
     private void resetStateLocked(boolean hideBouncer) {
-        if (DEBUG) Log.d(TAG, "resetStateLocked");
+        if (DEBUG) Log.e(TAG, "resetStateLocked");
         Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0);
         mHandler.sendMessage(msg);
     }
@@ -2827,18 +2746,6 @@
                     message = "BOOT_INTERACTOR";
                     handleBootInteractor();
                     break;
-                case BEFORE_USER_SWITCHING:
-                    message = "BEFORE_USER_SWITCHING";
-                    handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj);
-                    break;
-                case USER_SWITCHING:
-                    message = "USER_SWITCHING";
-                    handleUserSwitching(msg.arg1, (Runnable) msg.obj);
-                    break;
-                case USER_SWITCH_COMPLETE:
-                    message = "USER_SWITCH_COMPLETE";
-                    handleUserSwitchComplete(msg.arg1);
-                    break;
             }
             Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
         }
@@ -2980,9 +2887,6 @@
         mUiBgExecutor.execute(() -> {
             Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", "
                     + reason + ")");
-            if (showing) {
-                notifyLockNowCallback();
-            }
 
             if (KeyguardWmStateRefactor.isEnabled()) {
                 // Handled in WmLockscreenVisibilityManager if flag is enabled.
@@ -3027,7 +2931,6 @@
         synchronized (KeyguardViewMediator.this) {
             if (!mSystemReady) {
                 if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready.");
-                notifyLockNowCallback();
                 return;
             }
             if (DEBUG) Log.d(TAG, "handleShow");
@@ -3086,11 +2989,12 @@
         }
     }
 
-    final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
+    private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
         @SuppressLint("MissingPermission")
         @Override
         public void run() {
             Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable");
+            Log.d(TAG, "keyguardGoingAwayRunnable");
             mKeyguardViewControllerLazy.get().keyguardGoingAway();
 
             int flags = 0;
@@ -3127,10 +3031,6 @@
 
             // Handled in WmLockscreenVisibilityManager if flag is enabled.
             if (!KeyguardWmStateRefactor.isEnabled()) {
-                mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
-                Log.d(TAG, "keyguardGoingAway requested for userId: "
-                        + mGoingAwayRequestedForUserId);
-
                 // Don't actually hide the Keyguard at the moment, wait for window manager
                 // until it tells us it's safe to do so with startKeyguardExitAnimation.
                 // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager
@@ -3269,30 +3169,6 @@
             RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) {
         Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime
                 + " fadeoutDuration=" + fadeoutDuration);
-        int currentUserId = mSelectedUserInteractor.getSelectedUserId();
-        if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) {
-            Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId "
-                    + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: "
-                    + currentUserId);
-            if (finishedCallback != null) {
-                // There will not execute animation, send a finish callback to ensure the remote
-                // animation won't hang there.
-                try {
-                    finishedCallback.onAnimationFinished();
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onAnimationFinished", e);
-                }
-            }
-            mHiding = false;
-            if (mLockPatternUtils.isSecure(currentUserId)) {
-                doKeyguardLocked(null);
-            } else {
-                resetStateLocked();
-                dismiss(null, null);
-            }
-            return;
-        }
-
         synchronized (KeyguardViewMediator.this) {
             mIsKeyguardExitAnimationCanceled = false;
             // Tell ActivityManager that we canceled the keyguard animation if
@@ -3537,13 +3413,6 @@
      * app transition before finishing the current RemoteAnimation, or the keyguard being re-shown).
      */
     private void handleCancelKeyguardExitAnimation() {
-        if (!KeyguardWmStateRefactor.isEnabled()
-                && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) {
-            Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: "
-                    + mGoingAwayRequestedForUserId + ", current: "
-                    + mSelectedUserInteractor.getSelectedUserId());
-            setPendingLock(true);
-        }
         if (mPendingLock) {
             Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. "
                     + "There's a pending lock, so we were cancelled because the device was locked "
@@ -3644,7 +3513,6 @@
         mSurfaceBehindRemoteAnimationRequested = true;
 
         if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) {
-            mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
             startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */);
             return;
         }
@@ -3665,9 +3533,6 @@
 
             if (!KeyguardWmStateRefactor.isEnabled()) {
                 // Handled in WmLockscreenVisibilityManager.
-                mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
-                Log.d(TAG, "keyguardGoingAway requested for userId: "
-                        + mGoingAwayRequestedForUserId);
                 mActivityTaskManagerService.keyguardGoingAway(flags);
             }
         } catch (RemoteException e) {
@@ -4123,29 +3988,6 @@
         mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged);
     }
 
-    private void notifyLockNowCallback() {
-        List<LockNowCallback> callbacks;
-        synchronized (mLockNowCallbacks) {
-            callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks);
-            mLockNowCallbacks.clear();
-        }
-        Iterator<LockNowCallback> iter = callbacks.listIterator();
-        while (iter.hasNext()) {
-            LockNowCallback callback = iter.next();
-            iter.remove();
-            if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) {
-                Log.i(TAG, "Not notifying lockNowCallback due to user mismatch");
-                continue;
-            }
-            Log.i(TAG, "Notifying lockNowCallback");
-            try {
-                callback.mRemoteCallback.sendResult(null);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Could not issue LockNowCallback sendResult", e);
-            }
-        }
-    }
-
     private void notifyTrustedChangedLocked(boolean trusted) {
         int size = mKeyguardStateCallbacks.size();
         for (int i = size - 1; i >= 0; i--) {
@@ -4310,14 +4152,4 @@
             }
         };
     }
-
-    private class LockNowCallback {
-        final int mUserId;
-        final IRemoteCallback mRemoteCallback;
-
-        LockNowCallback(int userId, IRemoteCallback remoteCallback) {
-            mUserId = userId;
-            mRemoteCallback = remoteCallback;
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f85a23c..eb96c92 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.Flags.communalSceneKtfRefactor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -57,6 +58,7 @@
     keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    private val communalSettingsInteractor: CommunalSettingsInteractor,
     private val communalSceneInteractor: CommunalSceneInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     val deviceEntryInteractor: DeviceEntryInteractor,
@@ -116,6 +118,17 @@
         }
     }
 
+    @SuppressLint("MissingPermission")
+    private fun shouldTransitionToCommunal(
+        shouldShowCommunal: Boolean,
+        isCommunalAvailable: Boolean,
+    ) =
+        if (communalSettingsInteractor.isV2FlagEnabled()) {
+            shouldShowCommunal
+        } else {
+            isCommunalAvailable && dreamManager.canStartDreaming(false)
+        }
+
     @OptIn(FlowPreview::class)
     @SuppressLint("MissingPermission")
     private fun listenForDozingToDreaming() {
@@ -141,9 +154,10 @@
                 .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
                 .sample(
                     communalInteractor.isCommunalAvailable,
+                    communalInteractor.shouldShowCommunal,
                     communalSceneInteractor.isIdleOnCommunal,
                 )
-                .collect { (_, isCommunalAvailable, isIdleOnCommunal) ->
+                .collect { (_, isCommunalAvailable, shouldShowCommunal, isIdleOnCommunal) ->
                     val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
                     val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
                     val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value
@@ -177,11 +191,9 @@
                         if (!SceneContainerFlag.isEnabled) {
                             startTransitionTo(KeyguardState.GLANCEABLE_HUB)
                         }
-                    } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
-                        // Using false for isScreenOn as canStartDreaming returns false if any
-                        // dream, including doze, is active.
-                        // This case handles tapping the power button to transition through
-                        // dream -> off -> hub.
+                    } else if (
+                        shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+                    ) {
                         if (!SceneContainerFlag.isEnabled) {
                             transitionToGlanceableHub()
                         }
@@ -203,6 +215,7 @@
             powerInteractor.detailedWakefulness
                 .filterRelevantKeyguardStateAnd { it.isAwake() }
                 .sample(
+                    communalInteractor.shouldShowCommunal,
                     communalInteractor.isCommunalAvailable,
                     communalSceneInteractor.isIdleOnCommunal,
                     keyguardInteractor.biometricUnlockState,
@@ -212,6 +225,7 @@
                 .collect {
                     (
                         _,
+                        shouldShowCommunal,
                         isCommunalAvailable,
                         isIdleOnCommunal,
                         biometricUnlockState,
@@ -245,7 +259,9 @@
                                     ownerReason = "waking from dozing",
                                 )
                             }
-                        } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+                        } else if (
+                            shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+                        ) {
                             if (!SceneContainerFlag.isEnabled) {
                                 transitionToGlanceableHub()
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 251af11..c1c509b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -129,20 +129,37 @@
         if (!communalSettingsInteractor.isCommunalFlagEnabled()) return
         if (SceneContainerFlag.isEnabled) return
         scope.launch {
-            powerInteractor.isAwake
-                .debounce(50L)
-                .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
-                .sample(communalInteractor.isCommunalAvailable)
-                .collect { isCommunalAvailable ->
-                    if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
-                        // This case handles tapping the power button to transition through
-                        // dream -> off -> hub.
-                        communalSceneInteractor.snapToScene(
-                            newScene = CommunalScenes.Communal,
-                            loggingReason = "from dreaming to hub",
-                        )
+            if (communalSettingsInteractor.isV2FlagEnabled()) {
+                powerInteractor.isAwake
+                    .debounce(50L)
+                    .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
+                    .sample(communalInteractor.shouldShowCommunal)
+                    .collect { shouldShowCommunal ->
+                        if (shouldShowCommunal) {
+                            // This case handles tapping the power button to transition through
+                            // dream -> off -> hub.
+                            communalSceneInteractor.snapToScene(
+                                newScene = CommunalScenes.Communal,
+                                loggingReason = "from dreaming to hub",
+                            )
+                        }
                     }
-                }
+            } else {
+                powerInteractor.isAwake
+                    .debounce(50L)
+                    .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
+                    .sample(communalInteractor.isCommunalAvailable)
+                    .collect { isCommunalAvailable ->
+                        if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
+                            // This case handles tapping the power button to transition through
+                            // dream -> off -> hub.
+                            communalSceneInteractor.snapToScene(
+                                newScene = CommunalScenes.Communal,
+                                loggingReason = "from dreaming to hub",
+                            )
+                        }
+                    }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 382436c..5f82102 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -215,6 +215,7 @@
                 animator = null,
                 modeOnCanceled = TransitionModeOnCanceled.RESET,
             )
+        repository.nextLockscreenTargetState.value = DEFAULT_STATE
         startTransition(newTransition)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 5c03d65..c5127a5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -66,11 +66,14 @@
          * range. This function maps the [startTime] and [duration] into [0, 1], when this subset is
          * valid.
          *
+         * Note that [onStep] accepts a null return value. When null, no animation information will
+         * be emitted, effectively saying "do not change the value on this frame"
+         *
          * Note that [onCancel] isn't used when the scene framework is enabled.
          */
         fun sharedFlow(
-            duration: Duration,
-            onStep: (Float) -> Float,
+            duration: Duration = transitionDuration,
+            onStep: (Float) -> Float?,
             startTime: Duration = 0.milliseconds,
             onStart: (() -> Unit)? = null,
             onCancel: (() -> Float)? = null,
@@ -102,7 +105,7 @@
          */
         fun sharedFlowWithState(
             duration: Duration,
-            onStep: (Float) -> Float,
+            onStep: (Float) -> Float?,
             startTime: Duration = 0.milliseconds,
             onStart: (() -> Unit)? = null,
             onCancel: (() -> Float)? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
index 19cd501..50f8e08 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.transitions
 
+import android.util.MathUtils
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -33,8 +34,18 @@
     blurConfig: BlurConfig,
 ) {
     val exitBlurRadius: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+        transitionAnimation.sharedFlow(
+            onStep = { MathUtils.lerp(blurConfig.maxBlurRadiusPx, blurConfig.minBlurRadiusPx, it) },
+            onStart = { blurConfig.maxBlurRadiusPx },
+            onFinish = { blurConfig.minBlurRadiusPx },
+            onCancel = { blurConfig.maxBlurRadiusPx },
+        )
 
     val enterBlurRadius: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+        transitionAnimation.sharedFlow(
+            onStep = { MathUtils.lerp(blurConfig.minBlurRadiusPx, blurConfig.maxBlurRadiusPx, it) },
+            onStart = { blurConfig.minBlurRadiusPx },
+            onFinish = { blurConfig.maxBlurRadiusPx },
+            onCancel = { blurConfig.minBlurRadiusPx },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
index dc7fefa..65c0a4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
@@ -54,7 +54,12 @@
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(0f)
+        transitionAnimation.sharedFlow(
+            duration = TO_DOZING_DURATION,
+            onStep = { null },
+            onFinish = { 0f },
+            onCancel = { 0f },
+        )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest {
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
index 8469cb4..f8072f2 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
@@ -78,7 +78,7 @@
 
     @Provides
     @IntoSet
-    @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS)
+    @Named(LOW_LIGHT_PRECONDITIONS)
     static Condition provideLowLightCondition(LowLightCondition lowLightCondition,
             DirectBootCondition directBootCondition) {
         // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
index d63c2e0..0107a52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
@@ -23,11 +23,11 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewOutlineProvider
+import androidx.annotation.VisibleForTesting
 import androidx.core.view.GestureDetectorCompat
 import androidx.dynamicanimation.animation.FloatPropertyCompat
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.app.tracing.TraceStateLogger
-import com.android.internal.annotations.VisibleForTesting
 import com.android.settingslib.Utils
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
@@ -38,9 +38,10 @@
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.wm.shell.shared.animation.PhysicsAnimator
+import kotlin.math.sign
 
 private const val FLING_SLOP = 1000000
-private const val DISMISS_DELAY = 100L
+@VisibleForTesting const val DISMISS_DELAY = 100L
 private const val SCROLL_DELAY = 100L
 private const val RUBBERBAND_FACTOR = 0.2f
 private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
@@ -64,7 +65,7 @@
     private val closeGuts: (immediate: Boolean) -> Unit,
     private val falsingManager: FalsingManager,
     private val logSmartspaceImpression: (Boolean) -> Unit,
-    private val logger: MediaUiEventLogger
+    private val logger: MediaUiEventLogger,
 ) {
     /** Trace state logger for media carousel visibility */
     private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser")
@@ -96,7 +97,7 @@
 
     /** What's the currently visible player index? */
     var visibleMediaIndex: Int = 0
-        private set
+        @VisibleForTesting set
 
     /** How much are we scrolled into the current media? */
     private var scrollIntoCurrentMedia: Int = 0
@@ -137,14 +138,14 @@
                 eStart: MotionEvent?,
                 eCurrent: MotionEvent,
                 vX: Float,
-                vY: Float
+                vY: Float,
             ) = onFling(vX, vY)
 
             override fun onScroll(
                 down: MotionEvent?,
                 lastMotion: MotionEvent,
                 distanceX: Float,
-                distanceY: Float
+                distanceY: Float,
             ) = onScroll(down!!, lastMotion, distanceX)
 
             override fun onDown(e: MotionEvent): Boolean {
@@ -157,6 +158,7 @@
     val touchListener =
         object : Gefingerpoken {
             override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
+
             override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
         }
 
@@ -168,7 +170,7 @@
                 scrollX: Int,
                 scrollY: Int,
                 oldScrollX: Int,
-                oldScrollY: Int
+                oldScrollY: Int,
             ) {
                 if (playerWidthPlusPadding == 0) {
                     return
@@ -177,7 +179,7 @@
                 val relativeScrollX = scrollView.relativeScrollX
                 onMediaScrollingChanged(
                     relativeScrollX / playerWidthPlusPadding,
-                    relativeScrollX % playerWidthPlusPadding
+                    relativeScrollX % playerWidthPlusPadding,
                 )
             }
         }
@@ -209,7 +211,7 @@
                         0,
                         carouselWidth,
                         carouselHeight,
-                        cornerRadius.toFloat()
+                        cornerRadius.toFloat(),
                     )
                 }
             }
@@ -235,7 +237,7 @@
                     getMaxTranslation().toFloat(),
                     0.0f,
                     1.0f,
-                    Math.abs(contentTranslation)
+                    Math.abs(contentTranslation),
                 )
             val settingsTranslation =
                 (1.0f - settingsOffset) *
@@ -323,7 +325,7 @@
                         CONTENT_TRANSLATION,
                         newTranslation,
                         startVelocity = 0.0f,
-                        config = translationConfig
+                        config = translationConfig,
                     )
                     .start()
                 scrollView.animationTargetX = newTranslation
@@ -391,7 +393,7 @@
                         CONTENT_TRANSLATION,
                         newTranslation,
                         startVelocity = 0.0f,
-                        config = translationConfig
+                        config = translationConfig,
                     )
                     .start()
             } else {
@@ -430,7 +432,7 @@
                     CONTENT_TRANSLATION,
                     newTranslation,
                     startVelocity = vX,
-                    config = translationConfig
+                    config = translationConfig,
                 )
                 .start()
             scrollView.animationTargetX = newTranslation
@@ -583,10 +585,35 @@
         // We need to post this to wait for the active player becomes visible.
         mainExecutor.executeDelayed(
             { scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
-            SCROLL_DELAY
+            SCROLL_DELAY,
         )
     }
 
+    /**
+     * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond
+     * the carousel's bounds:
+     * - If the carousel is not dismissible, the settings button is displayed.
+     * - If the carousel is dismissible, no action taken.
+     *
+     * @param step A positive number means next, and negative means previous.
+     */
+    fun scrollByStep(step: Int) {
+        val destIndex = visibleMediaIndex + step
+        if (destIndex >= mediaContent.childCount || destIndex < 0) {
+            if (!showsSettingsButton) return
+            var translation = getMaxTranslation() * sign(-step.toFloat())
+            translation = if (isRtl) -translation else translation
+            PhysicsAnimator.getInstance(this)
+                .spring(CONTENT_TRANSLATION, translation, config = translationConfig)
+                .start()
+            scrollView.animationTargetX = translation
+        } else if (scrollView.getContentTranslation() != 0.0f) {
+            resetTranslation(true)
+        } else {
+            scrollToPlayer(destIndex = destIndex)
+        }
+    }
+
     companion object {
         private val CONTENT_TRANSLATION =
             object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
index 1b92510..9319961 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
@@ -24,7 +24,7 @@
 import com.android.compose.animation.scene.UserActionResult.ShowOverlay
 import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
 import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -38,7 +38,7 @@
             mapOf(
                 Swipe.Up to HideOverlay(Overlays.NotificationsShade),
                 Back to HideOverlay(Overlays.NotificationsShade),
-                Swipe.Down(fromSource = SceneContainerEdge.TopRight) to
+                Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
                     ShowOverlay(
                         Overlays.QuickSettingsShade,
                         hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
index 16dff7d..11b014c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.log.core.Logger
 import com.android.systemui.qs.panels.shared.model.PanelsLog
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
@@ -83,34 +84,78 @@
             .flowOn(backgroundDispatcher)
 
     /** Sets for the current user the set of [TileSpec] to display as large tiles. */
-    fun setLargeTilesSpecs(specs: Set<TileSpec>) {
-        setLargeTilesSpecsForUser(specs, userRepository.getSelectedUserInfo().id)
-    }
-
-    private fun setLargeTilesSpecsForUser(specs: Set<TileSpec>, userId: Int) {
-        with(getSharedPrefs(userId)) {
-            edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply()
+    fun writeLargeTileSpecs(specs: Set<TileSpec>) {
+        with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) {
+            writeLargeTileSpecs(specs)
+            setLargeTilesDefault(false)
         }
     }
 
+    suspend fun deleteLargeTileDataJob() {
+        userRepository.selectedUserInfo.collect { userInfo ->
+            getSharedPrefs(userInfo.id)
+                .edit()
+                .remove(LARGE_TILES_SPECS_KEY)
+                .remove(LARGE_TILES_DEFAULT_KEY)
+                .apply()
+        }
+    }
+
+    private fun SharedPreferences.writeLargeTileSpecs(specs: Set<TileSpec>) {
+        edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply()
+    }
+
     /**
-     * Sets the initial tiles as large, if there is no set in SharedPrefs for the [userId]. This is
-     * to be used when upgrading to a build that supports large/small tiles.
+     * Sets the initial set of large tiles. One of the following cases will happen:
+     * * If we are setting the default set (no value stored in settings for the list of tiles), set
+     *   the large tiles based on [defaultLargeTilesRepository]. We do this to signal future reboots
+     *   that we have performed the upgrade path once. In this case, we will mark that we set them
+     *   as the default in case a restore needs to modify them later.
+     * * If we got a list of tiles restored from a device and nothing has modified the list of
+     *   tiles, set all the restored tiles to large. Note that if we also restored a set of large
+     *   tiles before this was called, [LARGE_TILES_DEFAULT_KEY] will be false and we won't
+     *   overwrite it.
+     * * If we got a list of tiles from settings, we consider that we upgraded in place and then we
+     *   will set all those tiles to large IF there's no current set of large tiles.
      *
      * Even if largeTilesSpec is read Eagerly before we know if we are in an initial state, because
      * we are not writing the default values to the SharedPreferences, the file will not contain the
      * key and this call will succeed, as long as there hasn't been any calls to setLargeTilesSpecs
      * for that user before.
      */
-    fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, userId: Int) {
+    fun setInitialOrUpgradeLargeTiles(upgradePath: TilesUpgradePath, userId: Int) {
         with(getSharedPrefs(userId)) {
-            if (!contains(LARGE_TILES_SPECS_KEY)) {
-                logger.i("Setting upgraded large tiles for user $userId: $specs")
-                setLargeTilesSpecsForUser(specs, userId)
+            when (upgradePath) {
+                is TilesUpgradePath.DefaultSet -> {
+                    writeLargeTileSpecs(defaultLargeTilesRepository.defaultLargeTiles)
+                    logger.i("Large tiles set to default on init")
+                    setLargeTilesDefault(true)
+                }
+                is TilesUpgradePath.RestoreFromBackup -> {
+                    if (
+                        getBoolean(LARGE_TILES_DEFAULT_KEY, false) ||
+                            !contains(LARGE_TILES_SPECS_KEY)
+                    ) {
+                        writeLargeTileSpecs(upgradePath.value)
+                        logger.i("Tiles restored from backup set to large: ${upgradePath.value}")
+                        setLargeTilesDefault(false)
+                    }
+                }
+                is TilesUpgradePath.ReadFromSettings -> {
+                    if (!contains(LARGE_TILES_SPECS_KEY)) {
+                        writeLargeTileSpecs(upgradePath.value)
+                        logger.i("Tiles read from settings set to large: ${upgradePath.value}")
+                        setLargeTilesDefault(false)
+                    }
+                }
             }
         }
     }
 
+    private fun SharedPreferences.setLargeTilesDefault(value: Boolean) {
+        edit().putBoolean(LARGE_TILES_DEFAULT_KEY, value).apply()
+    }
+
     private fun getSharedPrefs(userId: Int): SharedPreferences {
         return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId)
     }
@@ -118,6 +163,7 @@
     companion object {
         private const val TAG = "QSPreferencesRepository"
         private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs"
+        private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default"
         const val FILE_NAME = "quick_settings_prefs"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
index 86838b4..9b98797 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
@@ -27,10 +28,20 @@
     val largeTilesSpecs: Flow<Set<TileSpec>> = repo.largeTilesSpecs
 
     fun setLargeTilesSpecs(specs: Set<TileSpec>) {
-        repo.setLargeTilesSpecs(specs)
+        repo.writeLargeTileSpecs(specs)
     }
 
-    fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, user: Int) {
-        repo.setInitialLargeTilesSpecs(specs, user)
+    /**
+     * This method should be called to indicate that a "new" set of tiles has been determined for a
+     * particular user coming from different upgrade sources.
+     *
+     * @see TilesUpgradePath for more information
+     */
+    fun setInitialOrUpgradeLargeTilesSpecs(specs: TilesUpgradePath, user: Int) {
+        repo.setInitialOrUpgradeLargeTiles(specs, user)
+    }
+
+    suspend fun deleteLargeTilesDataJob() {
+        repo.deleteLargeTileDataJob()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
index a8ac5c3..e279735 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
@@ -19,11 +19,13 @@
 import com.android.app.tracing.coroutines.launchTraced
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.flags.QsInCompose
 import com.android.systemui.qs.panels.domain.interactor.QSPreferencesInteractor
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
 
 class QSPanelsCoreStartable
 @Inject
@@ -33,10 +35,14 @@
     @Background private val backgroundApplicationScope: CoroutineScope,
 ) : CoreStartable {
     override fun start() {
-        backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") {
-            tileSpecRepository.tilesReadFromSetting.receiveAsFlow().collect { (tiles, userId) ->
-                preferenceInteractor.setInitialLargeTilesSpecs(tiles, userId)
+        if (QsInCompose.isEnabled) {
+            backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") {
+                tileSpecRepository.tilesUpgradePath.receiveAsFlow().collect { (tiles, userId) ->
+                    preferenceInteractor.setInitialOrUpgradeLargeTilesSpecs(tiles, userId)
+                }
             }
+        } else {
+            backgroundApplicationScope.launch { preferenceInteractor.deleteLargeTilesDataJob() }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index 6b7dd38..c50d5da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.res.R
 import com.android.systemui.retail.data.repository.RetailModeRepository
@@ -78,7 +79,7 @@
     /** Reset the current set of tiles to the default list of tiles */
     suspend fun resetToDefault(userId: Int)
 
-    val tilesReadFromSetting: ReceiveChannel<Pair<Set<TileSpec>, Int>>
+    val tilesUpgradePath: ReceiveChannel<Pair<TilesUpgradePath, Int>>
 
     companion object {
         /** Position to indicate the end of the list */
@@ -112,8 +113,8 @@
             .filter { it !is TileSpec.Invalid }
     }
 
-    private val _tilesReadFromSetting = Channel<Pair<Set<TileSpec>, Int>>(capacity = 5)
-    override val tilesReadFromSetting = _tilesReadFromSetting
+    private val _tilesUpgradePath = Channel<Pair<TilesUpgradePath, Int>>(capacity = 5)
+    override val tilesUpgradePath = _tilesUpgradePath
 
     private val userTileRepositories = SparseArray<UserTileSpecRepository>()
 
@@ -122,8 +123,8 @@
             val userTileRepository = userTileSpecRepositoryFactory.create(userId)
             userTileRepositories.put(userId, userTileRepository)
             applicationScope.launchTraced("TileSpecRepository.aggregateTilesPerUser") {
-                for (tilesFromSettings in userTileRepository.tilesReadFromSettings) {
-                    _tilesReadFromSetting.send(tilesFromSettings to userId)
+                for (tileUpgrade in userTileRepository.tilesUpgradePath) {
+                    _tilesUpgradePath.send(tileUpgrade to userId)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
index 7b56cd9..5aa5eda 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
@@ -9,6 +9,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.util.settings.SecureSettings
 import dagger.assisted.Assisted
@@ -49,8 +50,8 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
 
-    private val _tilesReadFromSettings = Channel<Set<TileSpec>>(capacity = 2)
-    val tilesReadFromSettings: ReceiveChannel<Set<TileSpec>> = _tilesReadFromSettings
+    private val _tilesUpgradePath = Channel<TilesUpgradePath>(capacity = 3)
+    val tilesUpgradePath: ReceiveChannel<TilesUpgradePath> = _tilesUpgradePath
 
     private val defaultTiles: List<TileSpec>
         get() = defaultTilesRepository.defaultTiles
@@ -67,14 +68,23 @@
                     .scan(loadTilesFromSettingsAndParse(userId)) { current, change ->
                         change
                             .apply(current)
-                            .also {
-                                if (current != it) {
+                            .also { afterRestore ->
+                                if (current != afterRestore) {
                                     if (change is RestoreTiles) {
-                                        logger.logTilesRestoredAndReconciled(current, it, userId)
+                                        logger.logTilesRestoredAndReconciled(
+                                            current,
+                                            afterRestore,
+                                            userId,
+                                        )
                                     } else {
-                                        logger.logProcessTileChange(change, it, userId)
+                                        logger.logProcessTileChange(change, afterRestore, userId)
                                     }
                                 }
+                                if (change is RestoreTiles) {
+                                    _tilesUpgradePath.send(
+                                        TilesUpgradePath.RestoreFromBackup(afterRestore.toSet())
+                                    )
+                                }
                             }
                             // Distinct preserves the order of the elements removing later
                             // duplicates,
@@ -154,7 +164,9 @@
     private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> {
         val loadedTiles = loadTilesFromSettings(userId)
         if (loadedTiles.isNotEmpty()) {
-            _tilesReadFromSettings.send(loadedTiles.toSet())
+            _tilesUpgradePath.send(TilesUpgradePath.ReadFromSettings(loadedTiles.toSet()))
+        } else {
+            _tilesUpgradePath.send(TilesUpgradePath.DefaultSet)
         }
         return parseTileSpecs(loadedTiles, userId)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt
new file mode 100644
index 0000000..98f30c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pipeline.shared
+
+/** Upgrade paths indicating the source of the list of QS tiles. */
+sealed interface TilesUpgradePath {
+
+    sealed interface UpgradeWithTiles : TilesUpgradePath {
+        val value: Set<TileSpec>
+    }
+
+    /** This indicates a set of tiles that was read from Settings on user start */
+    @JvmInline value class ReadFromSettings(override val value: Set<TileSpec>) : UpgradeWithTiles
+
+    /** This indicates a set of tiles that was restored from backup */
+    @JvmInline value class RestoreFromBackup(override val value: Set<TileSpec>) : UpgradeWithTiles
+
+    /**
+     * This indicates that no tiles were read from Settings on user start so the default has been
+     * stored.
+     */
+    data object DefaultSet : TilesUpgradePath
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
index 5bc26f5..52c4e2f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
@@ -25,7 +25,7 @@
 import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
 import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
 import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -47,7 +47,7 @@
                         put(Back, HideOverlay(Overlays.QuickSettingsShade))
                     }
                     put(
-                        Swipe.Down(fromSource = SceneContainerEdge.TopLeft),
+                        Swipe.Down(fromSource = SceneContainerArea.StartHalf),
                         ShowOverlay(
                             Overlays.NotificationsShade,
                             hideCurrentOverlays =
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index a4949ad..caa6161 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.scene
 
-import androidx.compose.ui.unit.dp
 import com.android.systemui.CoreStartable
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -99,15 +96,5 @@
                 transitionsBuilder = SceneContainerTransitions(),
             )
         }
-
-        @Provides
-        fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
-            return SplitEdgeDetector(
-                topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
-                // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
-                //  replace this constant with dynamic window insets.
-                edgeSize = 40.dp,
-            )
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a018283..ea11d20 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.scene
 
-import androidx.compose.ui.unit.dp
 import com.android.systemui.CoreStartable
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -121,15 +118,5 @@
                 transitionsBuilder = SceneContainerTransitions(),
             )
         }
-
-        @Provides
-        fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
-            return SplitEdgeDetector(
-                topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
-                // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
-                //  replace this constant with dynamic window insets.
-                edgeSize = 40.dp,
-            )
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 7a32491c..475c079 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -240,7 +240,13 @@
     ) {
         val currentSceneKey = currentScene.value
         val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
-        if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+        if (
+            !validateSceneChange(
+                from = currentSceneKey,
+                to = resolvedScene,
+                loggingReason = loggingReason,
+            )
+        ) {
             return
         }
 
@@ -249,6 +255,7 @@
         logger.logSceneChanged(
             from = currentSceneKey,
             to = resolvedScene,
+            sceneState = sceneState,
             reason = loggingReason,
             isInstant = false,
         )
@@ -272,13 +279,20 @@
                     familyResolver.resolvedScene.value
                 }
             } ?: toScene
-        if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+        if (
+            !validateSceneChange(
+                from = currentSceneKey,
+                to = resolvedScene,
+                loggingReason = loggingReason,
+            )
+        ) {
             return
         }
 
         logger.logSceneChanged(
             from = currentSceneKey,
             to = resolvedScene,
+            sceneState = null,
             reason = loggingReason,
             isInstant = true,
         )
@@ -489,11 +503,12 @@
      * Will throw a runtime exception for illegal states (for example, attempting to change to a
      * scene that's not part of the current scene framework configuration).
      *
+     * @param from The current scene being transitioned away from
      * @param to The desired destination scene to transition to
      * @param loggingReason The reason why the transition is requested, for logging purposes
      * @return `true` if the scene change is valid; `false` if it shouldn't happen
      */
-    private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean {
+    private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
         check(
             !shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings)
         ) {
@@ -503,6 +518,10 @@
             "Can't change scene to ${to.debugName} in split shade mode!"
         }
 
+        if (from == to) {
+            return false
+        }
+
         if (to !in repository.allContentKeys) {
             return false
         }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index 16c2ef5..d005858 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -45,23 +45,30 @@
         )
     }
 
-    fun logSceneChanged(from: SceneKey, to: SceneKey, reason: String, isInstant: Boolean) {
+    fun logSceneChanged(
+        from: SceneKey,
+        to: SceneKey,
+        sceneState: Any?,
+        reason: String,
+        isInstant: Boolean,
+    ) {
         logBuffer.log(
             tag = TAG,
             level = LogLevel.INFO,
             messageInitializer = {
-                str1 = from.toString()
-                str2 = to.toString()
-                str3 = reason
+                str1 = "${from.debugName} → ${to.debugName}"
+                str2 = reason
+                str3 = sceneState?.toString()
                 bool1 = isInstant
             },
             messagePrinter = {
                 buildString {
-                    append("Scene changed: $str1 → $str2")
+                    append("Scene changed: $str1")
+                    str3?.let { append(" (sceneState=$it)") }
                     if (isInstant) {
                         append(" (instant)")
                     }
-                    append(", reason: $str3")
+                    append(", reason: $str2")
                 }
             },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index f0f476e..364da5f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -30,19 +30,14 @@
 import com.android.systemui.res.R
 
 /** A view that can serve as the root of the main SysUI window. */
-open class WindowRootView(
-    context: Context,
-    attrs: AttributeSet?,
-) :
-    FrameLayout(
-        context,
-        attrs,
-    ) {
+open class WindowRootView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
 
     private lateinit var layoutInsetsController: LayoutInsetsController
     private var leftInset = 0
     private var rightInset = 0
 
+    private var previousInsets: WindowInsets? = null
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
 
@@ -66,11 +61,14 @@
     override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? {
         return LayoutParams(
             FrameLayout.LayoutParams.MATCH_PARENT,
-            FrameLayout.LayoutParams.MATCH_PARENT
+            FrameLayout.LayoutParams.MATCH_PARENT,
         )
     }
 
     override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
+        if (windowInsets == previousInsets) {
+            return windowInsets
+        }
         val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
         if (fitsSystemWindows) {
             val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
@@ -95,7 +93,7 @@
         leftInset = pairInsets.first
         rightInset = pairInsets.second
         applyMargins()
-        return windowInsets
+        return windowInsets.also { previousInsets = WindowInsets(it) }
     }
 
     fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) {
@@ -143,37 +141,22 @@
     interface LayoutInsetsController {
 
         /** Update the insets and calculate them accordingly. */
-        fun getinsets(
-            windowInsets: WindowInsets?,
-            displayCutout: DisplayCutout?,
-        ): Pair<Int, Int>
+        fun getinsets(windowInsets: WindowInsets?, displayCutout: DisplayCutout?): Pair<Int, Int>
     }
 
     private class LayoutParams : FrameLayout.LayoutParams {
         var ignoreRightInset = false
 
-        constructor(
-            width: Int,
-            height: Int,
-        ) : super(
-            width,
-            height,
-        )
+        constructor(width: Int, height: Int) : super(width, height)
 
         @SuppressLint("CustomViewStyleable")
-        constructor(
-            context: Context,
-            attrs: AttributeSet?,
-        ) : super(
-            context,
-            attrs,
-        ) {
+        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
             val obtainedAttributes =
                 context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout)
             ignoreRightInset =
                 obtainedAttributes.getBoolean(
                     R.styleable.StatusBarWindowView_Layout_ignoreRightInset,
-                    false
+                    false,
                 )
             obtainedAttributes.recycle()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
new file mode 100644
index 0000000..ede453db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/** Identifies an area of the [SceneContainer] to detect swipe gestures on. */
+sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) :
+    SwipeSource {
+    data object StartEdge :
+        SceneContainerArea(
+            resolveArea = {
+                if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge
+            }
+        )
+
+    data object StartHalf :
+        SceneContainerArea(
+            resolveArea = {
+                if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf
+            }
+        )
+
+    data object EndEdge :
+        SceneContainerArea(
+            resolveArea = {
+                if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge
+            }
+        )
+
+    data object EndHalf :
+        SceneContainerArea(
+            resolveArea = {
+                if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf
+            }
+        )
+
+    override fun resolve(layoutDirection: LayoutDirection): Resolved {
+        return resolveArea(layoutDirection)
+    }
+
+    sealed interface Resolved : SwipeSource.Resolved {
+        data object LeftEdge : Resolved
+
+        data object LeftHalf : Resolved
+
+        data object BottomEdge : Resolved
+
+        data object RightEdge : Resolved
+
+        data object RightHalf : Resolved
+    }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally
+ * detects the left and right halves of the screen (besides the edges).
+ *
+ * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved
+ * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will
+ * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right
+ * corner of the screen.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead.
+ * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and
+ * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf]
+ * and [SceneContainerArea.EndHalf] will be resolved appropriately to
+ * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf].
+ *
+ * @param edgeSize The fixed size of each edge.
+ */
+class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector {
+
+    private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+    override fun source(
+        layoutSize: IntSize,
+        position: IntOffset,
+        density: Density,
+        orientation: Orientation,
+    ): SceneContainerArea.Resolved {
+        val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation)
+        return when (fixedEdge) {
+            Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge
+            Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge
+            Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge
+            else -> {
+                // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need
+                // to detect swipes on the top edge, and consider them part of the right/left half.
+                if (position.x < layoutSize.width * 0.5f) {
+                    SceneContainerArea.Resolved.LeftHalf
+                } else {
+                    SceneContainerArea.Resolved.RightHalf
+                }
+            }
+        }
+    }
+}
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 fbcd8ea..01bcc24 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
@@ -19,6 +19,7 @@
 import android.view.MotionEvent
 import android.view.View
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.unit.dp
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.DefaultEdgeDetector
@@ -31,6 +32,8 @@
 import com.android.systemui.classifier.Classifier
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
@@ -62,12 +65,13 @@
     private val powerInteractor: PowerInteractor,
     shadeModeInteractor: ShadeModeInteractor,
     private val remoteInputInteractor: RemoteInputInteractor,
-    private val splitEdgeDetector: SplitEdgeDetector,
     private val logger: SceneLogger,
     hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
     val lightRevealScrim: LightRevealScrimViewModel,
     val wallpaperViewModel: WallpaperViewModel,
     keyguardInteractor: KeyguardInteractor,
+    val burnIn: AodBurnInViewModel,
+    val clock: KeyguardClockViewModel,
     @Assisted view: View,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
 ) : ExclusiveActivatable() {
@@ -85,16 +89,20 @@
     val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view)
 
     /**
-     * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+     * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the
      * [UserAction]s for this container.
      */
-    val edgeDetector: SwipeSourceDetector by
+    val swipeSourceDetector: SwipeSourceDetector by
         hydrator.hydratedStateOf(
-            traceName = "edgeDetector",
+            traceName = "swipeSourceDetector",
             initialValue = DefaultEdgeDetector,
             source =
                 shadeModeInteractor.shadeMode.map {
-                    if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+                    if (it is ShadeMode.Dual) {
+                        SceneContainerSwipeDetector(edgeSize = 40.dp)
+                    } else {
+                        DefaultEdgeDetector
+                    }
                 },
         )
 
@@ -237,6 +245,7 @@
             logger.logSceneChanged(
                 from = fromScene,
                 to = toScene,
+                sceneState = null,
                 reason = "user interaction",
                 isInstant = false,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
deleted file mode 100644
index f88bcb5..0000000
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.FixedSizeEdgeDetector
-import com.android.compose.animation.scene.SwipeSource
-import com.android.compose.animation.scene.SwipeSourceDetector
-
-/**
- * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
- * top-left and top-right.
- */
-enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
-    SwipeSource {
-    TopLeft(resolveEdge = { Resolved.TopLeft }),
-    TopRight(resolveEdge = { Resolved.TopRight }),
-    TopStart(
-        resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
-    ),
-    TopEnd(
-        resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
-    ),
-    Bottom(resolveEdge = { Resolved.Bottom }),
-    Left(resolveEdge = { Resolved.Left }),
-    Right(resolveEdge = { Resolved.Right }),
-    Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
-    End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
-
-    override fun resolve(layoutDirection: LayoutDirection): Resolved {
-        return resolveEdge(layoutDirection)
-    }
-
-    enum class Resolved : SwipeSource.Resolved {
-        TopLeft,
-        TopRight,
-        Bottom,
-        Left,
-        Right,
-    }
-}
-
-/**
- * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
- * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
- * may change during runtime.
- *
- * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
- * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
- * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
- * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
- * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
- * [SceneContainerEdge.Resolved.Right].
- *
- * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
- *   percentage) of screen width to consider the split point between "top-left" and "top-right"
- *   edges. It is called on each source detection event.
- * @param edgeSize The fixed size of each edge.
- */
-class SplitEdgeDetector(
-    val topEdgeSplitFraction: () -> Float,
-    val edgeSize: Dp,
-) : SwipeSourceDetector {
-
-    private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
-
-    override fun source(
-        layoutSize: IntSize,
-        position: IntOffset,
-        density: Density,
-        orientation: Orientation,
-    ): SceneContainerEdge.Resolved? {
-        val fixedEdge =
-            fixedEdgeDetector.source(
-                layoutSize,
-                position,
-                density,
-                orientation,
-            )
-        return when (fixedEdge) {
-            Edge.Resolved.Top -> {
-                val topEdgeSplitFraction = topEdgeSplitFraction()
-                require(topEdgeSplitFraction in 0f..1f) {
-                    "topEdgeSplitFraction must return a value between 0.0 and 1.0"
-                }
-                val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
-                if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
-                else SceneContainerEdge.Resolved.TopRight
-            }
-            Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
-            Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
-            Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
-            null -> null
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index a379ef7..305e71e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -520,7 +520,10 @@
         val glanceableHubV2 = communalSettingsInteractor.isV2FlagEnabled()
         if (
             !hubShowing &&
-                (touchOnNotifications || touchOnUmo || touchOnSmartspace || glanceableHubV2)
+                (touchOnNotifications ||
+                    touchOnUmo ||
+                    touchOnSmartspace ||
+                    !communalViewModel.swipeToHubEnabled())
         ) {
             logger.d({
                 "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " +
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 9a79e1a..ce48c85 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -31,16 +31,26 @@
 import android.provider.AlarmClock
 import android.view.DisplayCutout
 import android.view.View
+import android.view.ViewGroup
 import android.view.WindowInsets
 import android.widget.TextView
 import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.unit.dp
 import androidx.constraintlayout.motion.widget.MotionLayout
 import androidx.core.view.doOnLayout
+import androidx.core.view.isVisible
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.app.animation.Interpolators
 import com.android.settingslib.Utils
 import com.android.systemui.Dumpable
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.battery.BatteryMeterView.MODE_ESTIMATE
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.demomode.DemoMode
@@ -60,12 +70,15 @@
 import com.android.systemui.shade.carrier.ShadeCarrierGroupController
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
+import com.android.systemui.statusbar.core.NewStatusBarIcons
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
+import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryWithEstimate
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.NextAlarmController
@@ -76,6 +89,7 @@
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.flow.MutableStateFlow
 
 /**
  * Controller for QS header.
@@ -100,6 +114,7 @@
     private val shadeDisplaysRepositoryLazy: Lazy<ShadeDisplaysRepository>,
     private val variableDateViewControllerFactory: VariableDateViewController.Factory,
     @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController,
+    private val batteryViewModelFactory: BatteryViewModel.Factory,
     private val dumpManager: DumpManager,
     private val shadeCarrierGroupControllerBuilder: ShadeCarrierGroupController.Builder,
     private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
@@ -162,6 +177,8 @@
     private var lastInsets: WindowInsets? = null
     private var nextAlarmIntent: PendingIntent? = null
 
+    private val showBatteryEstimate = MutableStateFlow(false)
+
     private var qsDisabled = false
     private var visible = false
         set(value) {
@@ -323,10 +340,6 @@
 
     override fun onInit() {
         variableDateViewControllerFactory.create(date as VariableDateView).init()
-        batteryMeterViewController.init()
-
-        // battery settings same as in QS icons
-        batteryMeterViewController.ignoreTunerUpdates()
 
         val fgColor =
             Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
@@ -336,11 +349,36 @@
         iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
         iconManager.setTint(fgColor, bgColor)
 
-        batteryIcon.updateColors(
-            fgColor /* foreground */,
-            bgColor /* background */,
-            fgColor, /* single tone (current default) */
-        )
+        if (!NewStatusBarIcons.isEnabled) {
+            batteryMeterViewController.init()
+
+            // battery settings same as in QS icons
+            batteryMeterViewController.ignoreTunerUpdates()
+
+            batteryIcon.isVisible = true
+            batteryIcon.updateColors(
+                fgColor /* foreground */,
+                bgColor /* background */,
+                fgColor, /* single tone (current default) */
+            )
+        } else {
+            // Configure the compose battery view
+            val batteryComposeView =
+                ComposeView(mView.context).apply {
+                    setContent {
+                        val showBatteryEstimate by showBatteryEstimate.collectAsStateWithLifecycle()
+                        BatteryWithEstimate(
+                            modifier = Modifier.height(17.dp).wrapContentWidth(),
+                            viewModelFactory = batteryViewModelFactory,
+                            isDark = { true },
+                            showEstimate = showBatteryEstimate,
+                        )
+                    }
+                }
+            mView.requireViewById<ViewGroup>(R.id.hover_system_icons_container).apply {
+                addView(batteryComposeView, -1)
+            }
+        }
 
         carrierIconSlots =
             listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile))
@@ -474,7 +512,11 @@
 
     private fun updateBatteryMode() {
         qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let {
-            batteryIcon.setPercentShowMode(it)
+            if (NewStatusBarIcons.isEnabled) {
+                showBatteryEstimate.value = it == MODE_ESTIMATE
+            } else {
+                batteryIcon.setPercentShowMode(it)
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index b9df9f86..7d4b0ed 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -45,13 +45,17 @@
 import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl
+import com.android.systemui.window.dagger.WindowRootViewBlurModule
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import javax.inject.Provider
 
 /** Module for classes related to the notification shade. */
-@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class])
+@Module(
+    includes =
+        [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class]
+)
 abstract class ShadeModule {
     companion object {
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
index b155ada8..1f534a5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
@@ -111,10 +111,7 @@
         statusbarWidth: Int,
     ): ShadeElement {
         val xPercentage = motionEvent.x / statusbarWidth
-        val threshold = shadeInteractor.get().getTopEdgeSplitFraction()
-        return if (xPercentage < threshold) {
-            notificationElement.get()
-        } else qsShadeElement.get()
+        return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get()
     }
 
     private fun monitorDisplayRemovals(): Job {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index 6eaedd7..2b3e4b5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -34,7 +34,11 @@
     override fun animateCollapseQs(fullyCollapse: Boolean) {
         if (shadeInteractor.isQsExpanded.value) {
             val key =
-                if (fullyCollapse || shadeModeInteractor.isDualShade) {
+                if (
+                    fullyCollapse ||
+                        shadeModeInteractor.isDualShade ||
+                        shadeModeInteractor.isSplitShade
+                ) {
                     SceneFamilies.Home
                 } else {
                     Scenes.Shade
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index c8ce316..6d68796 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade.domain.interactor
 
-import androidx.annotation.FloatRange
 import com.android.compose.animation.scene.TransitionKey
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -66,16 +65,6 @@
      * wide as the entire screen.
      */
     val isShadeLayoutWide: StateFlow<Boolean>
-
-    /**
-     * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
-     * between "top-left" and "top-right" for the purposes of dual-shade invocation.
-     *
-     * Note that this fraction only determines the *split* between the absolute left and right
-     * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
-     * will resolve to "top-left".
-     */
-    @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
 }
 
 /** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index b1129a9..77e6a83 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -48,8 +48,6 @@
     override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
     override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
 
-    override fun getTopEdgeSplitFraction(): Float = 0.5f
-
     override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
 
     override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index 8f4e870..1ab0b93 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.scene.domain.SceneFrameworkTableLog
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
@@ -32,6 +33,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -89,10 +91,14 @@
 ) : ShadeModeInteractor {
 
     private val isDualShadeEnabled: Flow<Boolean> =
-        secureSettingsRepository.boolSetting(
-            Settings.Secure.DUAL_SHADE,
-            defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
-        )
+        if (SceneContainerFlag.isEnabled) {
+            secureSettingsRepository.boolSetting(
+                Settings.Secure.DUAL_SHADE,
+                defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
+            )
+        } else {
+            flowOf(false)
+        }
 
     override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide
 
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
index c6752f8..cf3b08c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
@@ -20,10 +20,11 @@
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ShowOverlay
 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.SceneContainerArea
 
 /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */
 fun singleShadeActions(
@@ -66,11 +67,10 @@
 
 /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */
 fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
-    val notifShadeUserActionResult = UserActionResult.ShowOverlay(Overlays.NotificationsShade)
-    val qsShadeuserActionResult = UserActionResult.ShowOverlay(Overlays.QuickSettingsShade)
     return arrayOf(
-        Swipe.Down to notifShadeUserActionResult,
-        Swipe.Down(fromSource = SceneContainerEdge.TopRight) to qsShadeuserActionResult,
+        Swipe.Down to ShowOverlay(Overlays.NotificationsShade),
+        Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
+            ShowOverlay(Overlays.QuickSettingsShade),
     )
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 25ebc8c..f06565f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -24,8 +24,10 @@
 import static android.os.Flags.allowPrivateProfile;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_NULL;
+import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY;
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
+import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI;
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
@@ -44,6 +46,7 @@
 import android.database.ExecutorContentObserver;
 import android.net.Uri;
 import android.os.Looper;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -118,6 +121,11 @@
             Settings.Secure.getUriFor(LOCK_SCREEN_SHOW_NOTIFICATIONS);
     private static final Uri SHOW_PRIVATE_LOCKSCREEN =
             Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+    private static final Uri REDACT_OTP_ON_WIFI =
+            Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI);
+
+    private static final Uri REDACT_OTP_IMMEDIATELY =
+            Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_IMMEDIATELY);
 
     private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS =
             TimeUnit.MINUTES.toMillis(10);
@@ -307,6 +315,9 @@
     @VisibleForTesting
     protected final AtomicBoolean mConnectedToWifi = new AtomicBoolean(false);
 
+    protected final AtomicBoolean mRedactOtpOnWifi = new AtomicBoolean(true);
+    protected final AtomicBoolean mRedactOtpImmediately = new AtomicBoolean(false);
+
     protected int mCurrentUserId = 0;
 
     protected NotificationPresenter mPresenter;
@@ -363,6 +374,8 @@
 
         mLockScreenUris.add(SHOW_LOCKSCREEN);
         mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN);
+        mLockScreenUris.add(REDACT_OTP_ON_WIFI);
+        mLockScreenUris.add(REDACT_OTP_IMMEDIATELY);
 
         dumpManager.registerDumpable(this);
 
@@ -432,6 +445,10 @@
                         changed |= updateUserShowSettings(user.getIdentifier());
                     } else if (SHOW_PRIVATE_LOCKSCREEN.equals(uri)) {
                         changed |= updateUserShowPrivateSettings(user.getIdentifier());
+                    } else if (REDACT_OTP_ON_WIFI.equals(uri)) {
+                        changed |= updateRedactOtpOnWifiSetting();
+                    } else if (REDACT_OTP_IMMEDIATELY.equals(uri)) {
+                        changed |= updateRedactOtpImmediatelySetting();
                     }
                 }
 
@@ -465,6 +482,14 @@
                 true,
                 mLockscreenSettingsObserver,
                 USER_ALL);
+        mSecureSettings.registerContentObserverAsync(
+                REDACT_OTP_ON_WIFI,
+                mLockscreenSettingsObserver
+        );
+        mSecureSettings.registerContentObserverAsync(
+                REDACT_OTP_IMMEDIATELY,
+                mLockscreenSettingsObserver
+        );
 
 
         mBroadcastDispatcher.registerReceiver(mAllUsersReceiver,
@@ -602,6 +627,28 @@
     }
 
     @WorkerThread
+    private boolean updateRedactOtpOnWifiSetting() {
+        boolean originalValue = mRedactOtpOnWifi.get();
+        boolean newValue = mSecureSettings.getIntForUser(
+                REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI,
+                0,
+                Process.myUserHandle().getIdentifier()) != 0;
+        mRedactOtpOnWifi.set(newValue);
+        return originalValue != newValue;
+    }
+
+    @WorkerThread
+    private boolean updateRedactOtpImmediatelySetting() {
+        boolean originalValue = mRedactOtpImmediately.get();
+        boolean newValue = mSecureSettings.getIntForUser(
+                REDACT_OTP_NOTIFICATION_IMMEDIATELY,
+                0,
+                Process.myUserHandle().getIdentifier()) != 0;
+        mRedactOtpImmediately.set(newValue);
+        return originalValue != newValue;
+    }
+
+    @WorkerThread
     private boolean updateGlobalKeyguardSettings() {
         final boolean oldValue = mKeyguardAllowingNotifications;
         mKeyguardAllowingNotifications = mKeyguardManager.getPrivateNotificationsAllowed();
@@ -769,23 +816,31 @@
             return false;
         }
 
-        if (mConnectedToWifi.get()) {
-            return false;
+        if (!mRedactOtpOnWifi.get()) {
+            if (mConnectedToWifi.get()) {
+                return false;
+            }
+
+            long lastWifiConnectTime = mLastWifiConnectionTime.get();
+            // If the device has connected to wifi since receiving the notification, do not redact
+            if (ent.getSbn().getPostTime() < lastWifiConnectTime) {
+                return false;
+            }
         }
 
         if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) {
             return false;
         }
 
-        long lastWifiConnectTime = mLastWifiConnectionTime.get();
-        // If the device has connected to wifi since receiving the notification, do not redact
-        if (ent.getSbn().getPostTime() < lastWifiConnectTime) {
-            return false;
+        long latestTimeForRedaction;
+        if (mRedactOtpImmediately.get()) {
+            latestTimeForRedaction = mLastLockTime.get();
+        } else {
+            // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS
+            // when this notification arrived, do not redact
+            latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS;
         }
 
-        // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS when
-        // this notification arrived, do not redact
-        long latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS;
         if (ent.getSbn().getPostTime() < latestTimeForRedaction) {
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 155049f..31fdec6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -93,6 +93,7 @@
     private int mPaddingBetweenElements;
     private int mNotGoneIndex;
     private boolean mHasItemsInStableShelf;
+    private boolean mAlignedToEnd;
     private int mScrollFastThreshold;
     private boolean mInteractive;
     private boolean mAnimationsEnabled = true;
@@ -412,8 +413,22 @@
     public boolean isAlignedToEnd() {
         if (!NotificationMinimalism.isEnabled()) {
             return false;
+        } else if (SceneContainerFlag.isEnabled()) {
+            return mAlignedToEnd;
+        } else {
+            return mAmbientState.getUseSplitShade();
         }
-        return mAmbientState.getUseSplitShade();
+    }
+
+    /** @see #isAlignedToEnd() */
+    public void setAlignedToEnd(boolean alignedToEnd) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            return;
+        }
+        if (mAlignedToEnd != alignedToEnd) {
+            mAlignedToEnd = alignedToEnd;
+            requestLayout();
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index a2c0226..f466278 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -32,9 +32,7 @@
 import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
 import com.android.systemui.statusbar.chips.StatusBarChipsLog
 import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
-import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
-import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
@@ -86,12 +84,7 @@
                                 OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon)
                             }
 
-                        val colors =
-                            if (StatusBarNotifChips.isEnabled && state.promotedContent != null) {
-                                state.promotedContent.toCustomColorsModel()
-                            } else {
-                                ColorsModel.Themed
-                            }
+                        val colors = ColorsModel.AccentThemed
 
                         // This block mimics OngoingCallController#updateChip.
                         if (state.startTimeMs <= 0L) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 8357df4..2d6102e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -27,7 +27,7 @@
 import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
 import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
-import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
@@ -85,8 +85,7 @@
                     contentDescription,
                 )
             }
-        val colors = this.promotedContent.toCustomColorsModel()
-
+        val colors = ColorsModel.SystemThemed
         val clickListener: () -> Unit = {
             // The notification pipeline needs everything to run on the main thread, so keep
             // this event on the main thread.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 456cd12..d41353b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.ui.binder
 
 import android.annotation.IdRes
+import android.content.Context
 import android.content.res.ColorStateList
 import android.graphics.drawable.GradientDrawable
 import android.view.View
@@ -32,6 +33,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
@@ -76,8 +78,10 @@
                 chipTimeView.setTextColor(textColor)
                 chipTextView.setTextColor(textColor)
                 chipShortTimeDeltaView.setTextColor(textColor)
-                (chipBackgroundView.background as GradientDrawable).color =
-                    chipModel.colors.background(chipContext)
+                (chipBackgroundView.background as GradientDrawable).setBackgroundColors(
+                    chipModel.colors,
+                    chipContext,
+                )
             }
             is OngoingActivityChipModel.Inactive -> {
                 // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
@@ -460,5 +464,20 @@
         chipView.minimumWidth = minimumWidth
     }
 
+    private fun GradientDrawable.setBackgroundColors(colors: ColorsModel, context: Context) {
+        this.color = colors.background(context)
+        val outline = colors.outline(context)
+        if (outline != null) {
+            this.setStroke(
+                context.resources.getDimensionPixelSize(
+                    R.dimen.ongoing_activity_chip_outline_width
+                ),
+                outline,
+            )
+        } else {
+            this.setStroke(0, /* color= */ 0)
+        }
+    }
+
     @IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
index 32de0fb..8443d10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
@@ -20,16 +20,9 @@
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
@@ -37,6 +30,8 @@
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.rememberTextMeasurer
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -83,15 +78,14 @@
                 softWrap = false,
                 modifier =
                     modifier
-                        .customTextContentLayout(
+                        .hideTextIfDoesNotFit(
+                            text = text,
+                            textStyle = textStyle,
+                            textMeasurer = textMeasurer,
                             maxTextWidth = maxTextWidth,
                             startPadding = startPadding,
                             endPadding = endPadding,
-                        ) { constraintWidth ->
-                            val intrinsicWidth =
-                                textMeasurer.measure(text, textStyle, softWrap = false).size.width
-                            intrinsicWidth <= constraintWidth
-                        }
+                        )
                         .neverDecreaseWidth(),
             )
         }
@@ -108,7 +102,6 @@
         }
 
         is OngoingActivityChipModel.Active.Text -> {
-            var hasOverflow by remember { mutableStateOf(false) }
             val text = viewModel.text
             Text(
                 text = text,
@@ -116,24 +109,14 @@
                 style = textStyle,
                 softWrap = false,
                 modifier =
-                    modifier
-                        .customTextContentLayout(
-                            maxTextWidth = maxTextWidth,
-                            startPadding = startPadding,
-                            endPadding = endPadding,
-                        ) { constraintWidth ->
-                            val intrinsicWidth =
-                                textMeasurer.measure(text, textStyle, softWrap = false).size.width
-                            hasOverflow = intrinsicWidth > constraintWidth
-                            constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f
-                        }
-                        .overflowFadeOut(
-                            hasOverflow = { hasOverflow },
-                            fadeLength =
-                                dimensionResource(
-                                    id = R.dimen.ongoing_activity_chip_text_fading_edge_length
-                                ),
-                        ),
+                    modifier.hideTextIfDoesNotFit(
+                        text = text,
+                        textStyle = textStyle,
+                        textMeasurer = textMeasurer,
+                        maxTextWidth = maxTextWidth,
+                        startPadding = startPadding,
+                        endPadding = endPadding,
+                    ),
             )
         }
 
@@ -180,45 +163,67 @@
 }
 
 /**
- * A custom layout modifier for text that ensures its text is only visible if a provided
- * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for
- * provided padding values if provided and ensures its text is placed with the provided padding
- * included around it.
+ * A custom layout modifier for text that ensures the text is only visible if it completely fits
+ * within the constrained bounds. Imposes a provided [maxTextWidthPx]. Also, accounts for provided
+ * padding values if provided and ensures its text is placed with the provided padding included
+ * around it.
  */
-private fun Modifier.customTextContentLayout(
+private fun Modifier.hideTextIfDoesNotFit(
+    text: String,
+    textStyle: TextStyle,
+    textMeasurer: TextMeasurer,
     maxTextWidth: Dp,
     startPadding: Dp = 0.dp,
     endPadding: Dp = 0.dp,
-    shouldShow: (constraintWidth: Int) -> Boolean,
 ): Modifier {
     return this.then(
-        CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow)
+        HideTextIfDoesNotFitElement(
+            text,
+            textStyle,
+            textMeasurer,
+            maxTextWidth,
+            startPadding,
+            endPadding,
+        )
     )
 }
 
-private data class CustomTextContentLayoutElement(
+private data class HideTextIfDoesNotFitElement(
+    val text: String,
+    val textStyle: TextStyle,
+    val textMeasurer: TextMeasurer,
     val maxTextWidth: Dp,
     val startPadding: Dp,
     val endPadding: Dp,
-    val shouldShow: (constrainedWidth: Int) -> Boolean,
-) : ModifierNodeElement<CustomTextContentLayoutNode>() {
-    override fun create(): CustomTextContentLayoutNode {
-        return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow)
+) : ModifierNodeElement<HideTextIfDoesNotFitNode>() {
+    override fun create(): HideTextIfDoesNotFitNode {
+        return HideTextIfDoesNotFitNode(
+            text,
+            textStyle,
+            textMeasurer,
+            maxTextWidth,
+            startPadding,
+            endPadding,
+        )
     }
 
-    override fun update(node: CustomTextContentLayoutNode) {
-        node.shouldShow = shouldShow
+    override fun update(node: HideTextIfDoesNotFitNode) {
+        node.text = text
+        node.textStyle = textStyle
+        node.textMeasurer = textMeasurer
         node.maxTextWidth = maxTextWidth
         node.startPadding = startPadding
         node.endPadding = endPadding
     }
 }
 
-private class CustomTextContentLayoutNode(
+private class HideTextIfDoesNotFitNode(
+    var text: String,
+    var textStyle: TextStyle,
+    var textMeasurer: TextMeasurer,
     var maxTextWidth: Dp,
     var startPadding: Dp,
     var endPadding: Dp,
-    var shouldShow: (constrainedWidth: Int) -> Boolean,
 ) : Modifier.Node(), LayoutModifierNode {
     override fun MeasureScope.measure(
         measurable: Measurable,
@@ -230,9 +235,10 @@
                 .coerceAtLeast(constraints.minWidth)
         val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth))
 
-        val height = placeable.height
-        val width = placeable.width
-        return if (shouldShow(maxWidth)) {
+        val intrinsicWidth = textMeasurer.measure(text, textStyle, softWrap = false).size.width
+        return if (intrinsicWidth <= maxWidth) {
+            val height = placeable.height
+            val width = placeable.width
             layout(width + horizontalPadding.roundToPx(), height) {
                 placeable.place(startPadding.roundToPx(), 0)
             }
@@ -241,20 +247,3 @@
         }
     }
 }
-
-private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier {
-    return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache {
-        val width = size.width
-        val start = (width - fadeLength.toPx()).coerceAtLeast(0f)
-        val gradient =
-            Brush.horizontalGradient(
-                colors = listOf(Color.Black, Color.Transparent),
-                startX = start,
-                endX = width,
-            )
-        onDrawWithContent {
-            drawContent()
-            if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
index 76c5386..1cdf680 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -19,6 +19,7 @@
 import android.content.res.ColorStateList
 import android.view.ViewGroup
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -103,6 +104,13 @@
         } else {
             dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding
         }
+
+    val outline = model.colors.outline(context)
+    val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width)
+
+    val shape =
+        RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius))
+
     // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
     // height of the chip is determined by the height of the background of the Row below.
     Box(
@@ -121,12 +129,7 @@
             horizontalArrangement = Arrangement.Center,
             verticalAlignment = Alignment.CenterVertically,
             modifier =
-                Modifier.clip(
-                        RoundedCornerShape(
-                            dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)
-                        )
-                    )
-                    .height(dimensionResource(R.dimen.ongoing_appops_chip_height))
+                Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height))
                     .thenIf(isClickable) { Modifier.widthIn(min = minWidth) }
                     .layout { measurable, constraints ->
                         val placeable = measurable.measure(constraints)
@@ -136,7 +139,14 @@
                             }
                         }
                     }
-                    .background(Color(model.colors.background(context).defaultColor))
+                    .background(Color(model.colors.background(context).defaultColor), shape = shape)
+                    .thenIf(outline != null) {
+                        Modifier.border(
+                            width = outlineWidth,
+                            color = Color(outline!!),
+                            shape = shape,
+                        )
+                    }
                     .padding(
                         horizontal =
                             if (hasEmbeddedIcon) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
index 25f90f9..4954cb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
@@ -21,7 +21,6 @@
 import androidx.annotation.ColorInt
 import com.android.settingslib.Utils
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 
 /** Model representing how the chip in the status bar should be colored. */
 sealed interface ColorsModel {
@@ -31,13 +30,38 @@
     /** The color for the text (and icon) on the chip. */
     @ColorInt fun text(context: Context): Int
 
-    /** The chip should match the theme's primary color. */
-    data object Themed : ColorsModel {
+    /** The color to use for the chip outline, or null if the chip shouldn't have an outline. */
+    @ColorInt fun outline(context: Context): Int?
+
+    /** The chip should match the theme's primary accent color. */
+    // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it
+    // only gets updated when a different configuration change happens, like a rotation.
+    data object AccentThemed : ColorsModel {
         override fun background(context: Context): ColorStateList =
             Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent)
 
         override fun text(context: Context) =
             Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+
+        override fun outline(context: Context) = null
+    }
+
+    /** The chip should match the system theme main color. */
+    // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it
+    // only gets updated when a different configuration change happens, like a rotation.
+    data object SystemThemed : ColorsModel {
+        override fun background(context: Context): ColorStateList =
+            ColorStateList.valueOf(
+                context.getColor(com.android.internal.R.color.materialColorSurfaceDim)
+            )
+
+        override fun text(context: Context) =
+            context.getColor(com.android.internal.R.color.materialColorOnSurface)
+
+        override fun outline(context: Context) =
+            // Outline is required on the SystemThemed chip to guarantee the chip doesn't completely
+            // blend in with the background.
+            context.getColor(com.android.internal.R.color.materialColorOutlineVariant)
     }
 
     /** The chip should have the given background color and primary text color. */
@@ -46,6 +70,8 @@
             ColorStateList.valueOf(backgroundColorInt)
 
         override fun text(context: Context): Int = primaryTextColorInt
+
+        override fun outline(context: Context) = null
     }
 
     /** The chip should have a red background with white text. */
@@ -55,15 +81,7 @@
         }
 
         override fun text(context: Context) = context.getColor(android.R.color.white)
-    }
 
-    companion object {
-        /** Converts the promoted notification colors to a [Custom] colors model. */
-        fun PromotedNotificationContentModel.toCustomColorsModel(): Custom {
-            return Custom(
-                backgroundColorInt = this.colors.backgroundColor,
-                primaryTextColorInt = this.colors.primaryTextColor,
-            )
-        }
+        override fun outline(context: Context) = null
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt
index 52495eb..c19b144 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt
@@ -51,9 +51,8 @@
     }
 
     /**
-     * Returns true if this view should show the text because there's enough room for a substantial
-     * amount of text, and returns false if this view should hide the text because the text is much
-     * too long.
+     * Returns true if this view should show the text because there's enough room for all the text,
+     * and returns false if this view should hide the text because not all of it fits.
      *
      * @param desiredTextWidthPx should be calculated by having the view measure itself with
      *   [unlimitedWidthMeasureSpec] and then sending its `measuredWidth` to this method. (This
@@ -82,9 +81,8 @@
             enforcedTextWidth = maxWidthBasedOnDimension
         }
 
-        // Only show the text if at least 50% of it can show. (Assume that if < 50% of the text will
-        // be visible, the text will be more confusing than helpful.)
-        return desiredTextWidthPx <= enforcedTextWidth * 2
+        // Only show the text if all of it can show
+        return desiredTextWidthPx <= enforcedTextWidth
     }
 
     private fun fetchMaxWidth() =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 3ba0ae3..1a30caf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -214,7 +214,6 @@
                     if (
                         secondaryChip is InternalChipModel.Active &&
                             StatusBarNotifChips.isEnabled &&
-                            !StatusBarChipsModernization.isEnabled &&
                             !isScreenReasonablyLarge
                     ) {
                         // If we have two showing chips and we don't have a ton of room
@@ -222,8 +221,10 @@
                         // possible so that we have the highest chance of showing both chips (as
                         // opposed to showing the primary chip with a lot of text and completely
                         // hiding the secondary chip).
-                        // Also: If StatusBarChipsModernization is enabled, then we'll do the
-                        // squishing in Compose instead.
+                        // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the
+                        // squishing in Compose instead, and be smart about it (e.g. if we have
+                        // room for the first chip to show text and the second chip to be icon-only,
+                        // do that instead of always squishing both chips.)
                         InternalMultipleOngoingActivityChipsModel(
                             primaryChip.squish(),
                             secondaryChip.squish(),
@@ -237,24 +238,31 @@
 
     /** Squishes the chip down to the smallest content possible. */
     private fun InternalChipModel.Active.squish(): InternalChipModel.Active {
-        return when (model) {
+        return if (model.shouldSquish()) {
+            InternalChipModel.Active(this.type, this.model.toIconOnly())
+        } else {
+            this
+        }
+    }
+
+    private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean {
+        return when (this) {
             // Icon-only is already maximum squished
-            is OngoingActivityChipModel.Active.IconOnly -> this
+            is OngoingActivityChipModel.Active.IconOnly,
             // Countdown shows just a single digit, so already maximum squished
-            is OngoingActivityChipModel.Active.Countdown -> this
-            // The other chips have icon+text, so we should hide the text
+            is OngoingActivityChipModel.Active.Countdown -> false
+            // The other chips have icon+text, so we can squish them by hiding text
             is OngoingActivityChipModel.Active.Timer,
             is OngoingActivityChipModel.Active.ShortTimeDelta,
-            is OngoingActivityChipModel.Active.Text ->
-                InternalChipModel.Active(this.type, this.model.toIconOnly())
+            is OngoingActivityChipModel.Active.Text -> true
         }
     }
 
     private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active {
         // If this chip doesn't have an icon, then it only has text and we should continue showing
         // its text. (This is theoretically impossible because
-        // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon, but protect
-        // against it just in case.)
+        // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and
+        // [shouldSquish] returns false for that model, but protect against it just in case.)
         val currentIcon = icon ?: return this
         return OngoingActivityChipModel.Active.IconOnly(
             key,
@@ -271,8 +279,38 @@
      */
     val chips: StateFlow<MultipleOngoingActivityChipsModel> =
         if (StatusBarChipsModernization.isEnabled) {
-            incomingChipBundle
-                .map { bundle -> rankChips(bundle) }
+            combine(
+                    incomingChipBundle.map { bundle -> rankChips(bundle) },
+                    isScreenReasonablyLarge,
+                ) { rankedChips, isScreenReasonablyLarge ->
+                    if (
+                        StatusBarNotifChips.isEnabled &&
+                            !isScreenReasonablyLarge &&
+                            rankedChips.active.filter { !it.isHidden }.size >= 2
+                    ) {
+                        // If we have at least two showing chips and we don't have a ton of room
+                        // (!isScreenReasonablyLarge), then we want to make both of them as small as
+                        // possible so that we have the highest chance of showing both chips (as
+                        // opposed to showing the first chip with a lot of text and completely
+                        // hiding the other chips).
+                        val squishedActiveChips =
+                            rankedChips.active.map {
+                                if (!it.isHidden && it.shouldSquish()) {
+                                    it.toIconOnly()
+                                } else {
+                                    it
+                                }
+                            }
+
+                        MultipleOngoingActivityChipsModel(
+                            active = squishedActiveChips,
+                            overflow = rankedChips.overflow,
+                            inactive = rankedChips.inactive,
+                        )
+                    } else {
+                        rankedChips
+                    }
+                }
                 .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel())
         } else {
             MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow()
diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
similarity index 61%
copy from core/java/com/android/internal/app/IAppOpsCallback.aidl
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 3a9525c..37485fe 100644
--- a/core/java/com/android/internal/app/IAppOpsCallback.aidl
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.internal.app;
+package com.android.systemui.statusbar.notification.collection;
 
-// This interface is also used by native code, so must
-// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h
-oneway interface IAppOpsCallback {
-    void opChanged(int op, int uid, String packageName, String persistentDeviceId);
+/**
+ * Abstract class to represent notification section bundled by AI.
+ */
+public class BundleEntry extends PipelineEntry {
+
+    public class BundleEntryAdapter implements EntryAdapter {
+    }
 }
diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
similarity index 61%
copy from core/java/com/android/internal/app/IAppOpsCallback.aidl
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index 3a9525c..b12b1c5 100644
--- a/core/java/com/android/internal/app/IAppOpsCallback.aidl
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.internal.app;
+package com.android.systemui.statusbar.notification.collection;
 
-// This interface is also used by native code, so must
-// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h
-oneway interface IAppOpsCallback {
-    void opChanged(int op, int uid, String packageName, String persistentDeviceId);
+/**
+ * Adapter interface for UI to get relevant info.
+ */
+public interface EntryAdapter {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
index 915057f..c8e3be4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
@@ -27,7 +27,7 @@
  * Abstract superclass for top-level entries, i.e. things that can appear in the final notification
  * list shown to users. In practice, this means either GroupEntries or NotificationEntries.
  */
-public abstract class ListEntry {
+public abstract class ListEntry extends PipelineEntry {
     private final String mKey;
     private final long mCreationTime;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 09cc3f2..7dd82a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -270,6 +270,9 @@
         setRanking(ranking);
     }
 
+    public class NotifEntryAdapter implements EntryAdapter {
+    }
+
     @Override
     public NotificationEntry getRepresentativeEntry() {
         return this;
@@ -643,6 +646,10 @@
         return row.isMediaRow();
     }
 
+    public boolean containsCustomViews() {
+        return getSbn().getNotification().containsCustomViews();
+    }
+
     public void resetUserExpansion() {
         if (row != null) row.resetUserExpansion();
     }
diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
similarity index 61%
rename from core/java/com/android/internal/app/IAppOpsCallback.aidl
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
index 3a9525c..efedfef 100644
--- a/core/java/com/android/internal/app/IAppOpsCallback.aidl
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.internal.app;
+package com.android.systemui.statusbar.notification.collection;
 
-// This interface is also used by native code, so must
-// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h
-oneway interface IAppOpsCallback {
-    void opChanged(int op, int uid, String packageName, String persistentDeviceId);
+/**
+ * Class to represent a notification, group, or bundle in the pipeline.
+ */
+public class PipelineEntry {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
index 6ceeb6a..bcaf187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
@@ -26,7 +26,7 @@
  * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController].
  */
 @Module
-interface NotificationStackGoogleModule {
+interface NotificationStackModule {
     @Binds
     fun bindNotificationStackRebindingHider(
         impl: NotificationStackRebindingHiderImpl
@@ -35,7 +35,7 @@
 
 /** This is meant to be used by all SystemUI variants, also those without NSSL. */
 @Module
-interface NotificationStackModule {
+interface NotificationStackOptionalModule {
     @BindsOptionalOf
     fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index e10825b..34f4969 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -121,7 +121,7 @@
         NotificationMemoryModule.class,
         NotificationStatsLoggerModule.class,
         NotificationsLogModule.class,
-        NotificationStackModule.class,
+        NotificationStackOptionalModule.class,
 })
 public interface NotificationsModule {
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt
index de113d3..ccc2dff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt
@@ -49,7 +49,7 @@
 ) : Dumpable {
 
     private val tag = "AvalancheController"
-    private val debug = Compile.IS_DEBUG
+    private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
     var baseEntryMapStr: () -> String = { "baseEntryMapStr not initialized" }
 
     var enableAtRuntime = true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 6491223..f9e9bee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -12,7 +12,7 @@
 import com.android.systemui.util.children
 
 /** Walks view hiearchy of a given notification to estimate its memory use. */
-internal object NotificationMemoryViewWalker {
+object NotificationMemoryViewWalker {
 
     private const val TAG = "NotificationMemory"
 
@@ -26,9 +26,13 @@
         private var softwareBitmaps = 0
 
         fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+
         fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+
         fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+
         fun addStyle(styleUse: Int) = apply { style += styleUse }
+
         fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
             softwareBitmaps += softwareBitmapUse
         }
@@ -67,14 +71,14 @@
                     getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
                     getViewUsage(
                         ViewType.PRIVATE_CONTRACTED_VIEW,
-                        row.privateLayout?.contractedChild
+                        row.privateLayout?.contractedChild,
                     ),
                     getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
                     getViewUsage(
                         ViewType.PUBLIC_VIEW,
                         row.publicLayout?.expandedChild,
                         row.publicLayout?.contractedChild,
-                        row.publicLayout?.headsUpChild
+                        row.publicLayout?.headsUpChild,
                     ),
                 )
                 .filterNotNull()
@@ -107,14 +111,14 @@
             row.publicLayout?.expandedChild,
             row.publicLayout?.contractedChild,
             row.publicLayout?.headsUpChild,
-            seenObjects = seenObjects
+            seenObjects = seenObjects,
         )
     }
 
     private fun getViewUsage(
         type: ViewType,
         vararg rootViews: View?,
-        seenObjects: HashSet<Int> = hashSetOf()
+        seenObjects: HashSet<Int> = hashSetOf(),
     ): NotificationViewUsage? {
         val usageBuilder = lazy { UsageBuilder() }
         rootViews.forEach { rootView ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
index 7c75983..777ffda 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
@@ -231,6 +231,7 @@
     ) {
         // Icon binding must be called in this order
         updateImageView(icon, content.smallIcon)
+        icon?.setImageLevel(content.iconLevel)
         icon?.setBackgroundColor(Background.colorInt)
         icon?.originalIconColor = PrimaryText.colorInt
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index cd78722..39c7df0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -96,6 +96,7 @@
         contentBuilder.wasPromotedAutomatically =
             notification.extras.getBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false)
         contentBuilder.smallIcon = notification.smallIconModel(imageModelProvider)
+        contentBuilder.iconLevel = notification.iconLevel
         contentBuilder.appName = notification.loadHeaderAppName(context)
         contentBuilder.subText = notification.subText()
         contentBuilder.time = notification.extractWhen()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index af5a820..38d41e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -38,6 +38,7 @@
      */
     val wasPromotedAutomatically: Boolean,
     val smallIcon: ImageModel?,
+    val iconLevel: Int,
     val appName: CharSequence?,
     val subText: CharSequence?,
     val shortCriticalText: String?,
@@ -67,6 +68,7 @@
     class Builder(val key: String) {
         var wasPromotedAutomatically: Boolean = false
         var smallIcon: ImageModel? = null
+        var iconLevel: Int = 0
         var appName: CharSequence? = null
         var subText: CharSequence? = null
         var time: When? = null
@@ -94,6 +96,7 @@
                 identity = Identity(key, style),
                 wasPromotedAutomatically = wasPromotedAutomatically,
                 smallIcon = smallIcon,
+                iconLevel = iconLevel,
                 appName = appName,
                 subText = subText,
                 shortCriticalText = shortCriticalText,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 76ba7f9..2bc4874 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -106,7 +106,7 @@
         @Override
         public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
                 float startVelocity) {
-            cancelMagneticAnimations();
+            cancelTranslationAnimations();
             mMagneticAnimator.setSpring(springForce);
             mMagneticAnimator.setStartVelocity(startVelocity);
             mMagneticAnimator.animateToFinalPosition(endTranslation);
@@ -114,11 +114,15 @@
 
         @Override
         public void cancelMagneticAnimations() {
-            cancelTranslationAnimations();
             mMagneticAnimator.cancel();
         }
 
         @Override
+        public void cancelTranslationAnimations() {
+            ExpandableView.this.cancelTranslationAnimations();
+        }
+
+        @Override
         public boolean canRowBeDismissed() {
             return canExpandableViewBeDismissed();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c7e15fd..73e8246 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -901,6 +901,13 @@
         if (!satisfiesMinHeightRequirement(view, entry, resources)) {
             return "inflated notification does not meet minimum height requirement";
         }
+
+        if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+            if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+                return "inflated notification does not meet maximum memory size requirement";
+            }
+        }
+
         return null;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
new file mode 100644
index 0000000..c55cb67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.os.Build;
+
+/**
+ * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}.
+ */
+final class NotificationCustomContentCompat {
+    /**
+     * Enables memory size checking of custom views included in notifications to ensure that
+     * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes`
+     * config.xml parameter.
+     * Notifications exceeding the size will be rejected.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+    public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
new file mode 100644
index 0000000..a3e6a5c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.compat.CompatChanges
+import android.content.Context
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.traceSection
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Checks whether Notifications with Custom content views conform to configured memory limits. */
+object NotificationCustomContentMemoryVerifier {
+
+    private const val NOTIFICATION_SERVICE_TAG = "NotificationService"
+
+    /** Notifications with custom views need to conform to maximum memory consumption. */
+    @JvmStatic
+    fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean {
+        if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) {
+            return false
+        }
+
+        return entry.containsCustomViews()
+    }
+
+    /**
+     * This walks the custom view hierarchy contained in the passed Notification view and determines
+     * if the total memory consumption of all image views satisfies the limit set by
+     * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds
+     * [getWarnViewSizeLimit].
+     *
+     * @return true if the Notification conforms to the view size limits.
+     */
+    @JvmStatic
+    fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean {
+        val mainColumnView =
+            view.findViewById<View>(com.android.internal.R.id.notification_main_column)
+        if (mainColumnView == null) {
+            Log.wtf(
+                NOTIFICATION_SERVICE_TAG,
+                "R.id.notification_main_column view should not be null!",
+            )
+            return true
+        }
+
+        val memorySize =
+            traceSection("computeViewHiearchyImageViewSize") {
+                computeViewHierarchyImageViewSize(view)
+            }
+
+        if (memorySize > getStripViewSizeLimit(view.context)) {
+            val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid)
+            if (stripOversizedView) {
+                Log.w(
+                    NOTIFICATION_SERVICE_TAG,
+                    "Dropped notification due to too large RemoteViews ($memorySize bytes) on " +
+                        "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}",
+                )
+            } else {
+                Log.w(
+                    NOTIFICATION_SERVICE_TAG,
+                    "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+                        "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+                        "this WILL notification WILL be dropped when targetSdk " +
+                        "is set to ${Build.VERSION_CODES.BAKLAVA}!",
+                )
+            }
+
+            // We still warn for size, but return "satisfies = ok" if the target SDK
+            // is too low.
+            return !stripOversizedView
+        }
+
+        if (memorySize > getWarnViewSizeLimit(view.context)) {
+            // We emit the same warning as NotificationManagerService does to keep some consistency
+            // for developers.
+            Log.w(
+                NOTIFICATION_SERVICE_TAG,
+                "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+                    "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+                    "this notifications might be dropped in a future release",
+            )
+        }
+        return true
+    }
+
+    private fun isCompatChangeEnabledForUid(uid: Int): Boolean =
+        try {
+            CompatChanges.isChangeEnabled(
+                NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS,
+                uid,
+            )
+        } catch (e: RuntimeException) {
+            Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.")
+            false
+        }
+
+    @VisibleForTesting
+    @JvmStatic
+    fun computeViewHierarchyImageViewSize(view: View): Int =
+        when (view) {
+            is ViewGroup -> {
+                var use = 0
+                for (i in 0 until view.childCount) {
+                    use += computeViewHierarchyImageViewSize(view.getChildAt(i))
+                }
+                use
+            }
+            is ImageView -> computeImageViewSize(view)
+            else -> 0
+        }
+
+    /**
+     * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view
+     * contains any other kind of drawable, the memory size is estimated from its intrinsic
+     * dimensions.
+     *
+     * @return Bitmap size in bytes or 0 if no drawable is set.
+     */
+    private fun computeImageViewSize(view: ImageView): Int {
+        val drawable = view.drawable
+        return computeDrawableSize(drawable)
+    }
+
+    private fun computeDrawableSize(drawable: Drawable?): Int {
+        return when (drawable) {
+            null -> 0
+            is AdaptiveIconDrawable ->
+                computeDrawableSize(drawable.foreground) +
+                    computeDrawableSize(drawable.background) +
+                    computeDrawableSize(drawable.monochrome)
+            is BitmapDrawable -> drawable.bitmap.allocationByteCount
+            // People can sneak large drawables into those custom memory views via resources -
+            // we use the intrisic size as a proxy for how much memory rendering those will
+            // take.
+            else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4
+        }
+    }
+
+    /** @return Size of remote views after which a size warning is logged. */
+    @VisibleForTesting
+    fun getWarnViewSizeLimit(context: Context): Int =
+        context.resources.getInteger(
+            com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes
+        )
+
+    /** @return Size of remote views after which the notification is dropped. */
+    @VisibleForTesting
+    fun getStripViewSizeLimit(context: Context): Int =
+        context.resources.getInteger(
+            com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 20c3464..589e5b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -1396,9 +1396,17 @@
          */
         @VisibleForTesting
         fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? {
-            return if (!satisfiesMinHeightRequirement(view, entry, resources)) {
-                "inflated notification does not meet minimum height requirement"
-            } else null
+            if (!satisfiesMinHeightRequirement(view, entry, resources)) {
+                return "inflated notification does not meet minimum height requirement"
+            }
+
+            if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+                if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+                    return "inflated notification does not meet maximum memory size requirement"
+                }
+            }
+
+            return null
         }
 
         private fun satisfiesMinHeightRequirement(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
index 9fdd0bc..0703f2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
@@ -21,11 +21,14 @@
 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationShelf
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 
 /** Interactor for the [NotificationShelf] */
 @SysUISingleton
@@ -35,6 +38,7 @@
     private val keyguardRepository: KeyguardRepository,
     private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
     private val powerInteractor: PowerInteractor,
+    private val shadeModeInteractor: ShadeModeInteractor,
     private val keyguardTransitionController: LockscreenShadeTransitionController,
 ) {
     /** Is the shelf showing on the keyguard? */
@@ -51,6 +55,16 @@
                 isKeyguardShowing && isBypassEnabled
             }
 
+    /** Should the shelf be aligned to the end in the current configuration? */
+    val isAlignedToEnd: Flow<Boolean>
+        get() =
+            shadeModeInteractor.shadeMode.map { shadeMode ->
+                when (shadeMode) {
+                    ShadeMode.Split -> true
+                    else -> false
+                }
+            }
+
     /** Transition keyguard to the locked shade, triggered by the shelf. */
     fun goToLockedShadeFromShelf() {
         powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 0352a30..f663ea0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -16,15 +16,16 @@
 
 package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
 
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.app.tracing.traceSection
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
 object NotificationShelfViewBinder {
@@ -41,6 +42,11 @@
                 viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
             }
             launch { viewModel.isClickable.collect(::setCanInteract) }
+
+            if (SceneContainerFlag.isEnabled) {
+                launch { viewModel.isAlignedToEnd.collect(::setAlignedToEnd) }
+            }
+
             registerViewListenersWhileAttached(shelf, viewModel)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 5ca8b53..96cdda6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -17,11 +17,13 @@
 package com.android.systemui.statusbar.notification.shelf.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** ViewModel for [NotificationShelf]. */
@@ -40,6 +42,15 @@
     val canModifyColorOfNotifications: Flow<Boolean>
         get() = interactor.isShelfStatic.map { static -> !static }
 
+    /** Is the shelf aligned to the end in the current configuration? */
+    val isAlignedToEnd: Flow<Boolean> by lazy {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            interactor.isAlignedToEnd
+        }
+    }
+
     /** Notifies that the user has clicked the shelf. */
     fun onShelfClicked() {
         interactor.goToLockedShadeFromShelf()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index 3941700..5a29a69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -97,6 +97,7 @@
                 stackScrollLayout,
                 MAGNETIC_TRANSLATION_MULTIPLIERS.size,
             )
+        currentMagneticListeners.swipedListener()?.cancelTranslationAnimations()
         newListeners.forEach {
             if (currentMagneticListeners.contains(it)) {
                 it?.cancelMagneticAnimations()
@@ -214,22 +215,32 @@
     }
 
     override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) {
-        if (!row.isSwipedTarget()) return
-
-        when (currentState) {
-            State.PULLING -> {
-                snapNeighborsBack(velocity)
-                currentState = State.IDLE
+        if (row.isSwipedTarget()) {
+            when (currentState) {
+                State.PULLING -> {
+                    snapNeighborsBack(velocity)
+                    currentState = State.IDLE
+                }
+                State.DETACHED -> {
+                    // Cancel any detaching animation that may be occurring
+                    currentMagneticListeners.swipedListener()?.cancelMagneticAnimations()
+                    currentState = State.IDLE
+                }
+                else -> {}
             }
-            State.DETACHED -> {
-                currentState = State.IDLE
-            }
-            else -> {}
+        } else {
+            // A magnetic neighbor may be dismissing. In this case, we need to cancel any snap back
+            // magnetic animation to let the external dismiss animation proceed.
+            val listener = currentMagneticListeners.find { it == row.magneticRowListener }
+            listener?.cancelMagneticAnimations()
         }
     }
 
     override fun reset() {
-        currentMagneticListeners.forEach { it?.cancelMagneticAnimations() }
+        currentMagneticListeners.forEach {
+            it?.cancelMagneticAnimations()
+            it?.cancelTranslationAnimations()
+        }
         currentState = State.IDLE
         currentMagneticListeners = listOf()
         currentRoundableTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
index 46036d4..5959ef1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
@@ -42,6 +42,9 @@
     /** Cancel any animations related to the magnetic interactions of the row */
     fun cancelMagneticAnimations()
 
+    /** Cancel any other animations related to the row's translation */
+    fun cancelTranslationAnimations()
+
     /** Can the row be dismissed. */
     fun canRowBeDismissed(): Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 48a6a4c..810d0b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -463,6 +463,13 @@
                 }
 
                 @Override
+                public void onMagneticInteractionEnd(View view, float velocity) {
+                    if (view instanceof ExpandableNotificationRow row) {
+                        mMagneticNotificationRowManager.onMagneticInteractionEnd(row, velocity);
+                    }
+                }
+
+                @Override
                 public float getTotalTranslationLength(View animView) {
                     return mView.getTotalTranslationLength(animView);
                 }
@@ -504,14 +511,6 @@
                 public void onDragCancelled(View v) {
                 }
 
-                @Override
-                public void onDragCancelledWithVelocity(View v, float finalVelocity) {
-                    if (v instanceof ExpandableNotificationRow row) {
-                        mMagneticNotificationRowManager.onMagneticInteractionEnd(
-                                row, finalVelocity);
-                    }
-                }
-
                 /**
                  * Handles cleanup after the given {@code view} has been fully swiped out (including
                  * re-invoking dismiss logic in case the notification has not made its way out yet).
@@ -539,10 +538,6 @@
                  */
 
                 public void handleChildViewDismissed(View view) {
-                    if (view instanceof ExpandableNotificationRow row) {
-                        mMagneticNotificationRowManager.onMagneticInteractionEnd(
-                                row, null /* velocity */);
-                    }
                     // The View needs to clean up the Swipe states, e.g. roundness.
                     mView.onSwipeEnd();
                     if (mView.getClearAllInProgress()) {
@@ -614,11 +609,15 @@
 
                 @Override
                 public void onBeginDrag(View v) {
+                    mView.onSwipeBegin(v);
+                }
+
+                @Override
+                public void setMagneticAndRoundableTargets(View v) {
                     if (v instanceof ExpandableNotificationRow row) {
                         mMagneticNotificationRowManager.setMagneticAndRoundableTargets(
                                 row, mView, mSectionsManager);
                     }
-                    mView.onSwipeBegin(v);
                 }
 
                 @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index d476d48..6f4047f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -362,7 +362,8 @@
             superSnapChild(animView, targetLeft, velocity);
         }
 
-        mCallback.onDragCancelledWithVelocity(animView, velocity);
+        mCallback.onMagneticInteractionEnd(animView, velocity);
+        mCallback.onDragCancelled(animView);
         if (targetLeft == 0) {
             handleMenuCoveredOrDismissed();
         }
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 1bcc5ad..54efa4a 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
@@ -478,7 +478,7 @@
 
     /**
      * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
-     * notifications unless in splitshade.
+     * notifications unless it's a large screen.
      */
     private val alphaForShadeAndQsExpansion: Flow<Float> =
         if (SceneContainerFlag.isEnabled) {
@@ -501,16 +501,26 @@
                         Split -> isAnyExpanded.filter { it }.map { 1f }
                         Dual ->
                             combineTransform(
+                                shadeModeInteractor.isShadeLayoutWide,
                                 headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
                                 shadeInteractor.shadeExpansion,
                                 shadeInteractor.qsExpansion,
-                            ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
-                                if (isHeadsUpOrAnimatingAway) {
+                            ) {
+                                isShadeLayoutWide,
+                                isHeadsUpOrAnimatingAway,
+                                shadeExpansion,
+                                qsExpansion ->
+                                if (isShadeLayoutWide) {
+                                    if (shadeExpansion > 0f) {
+                                        emit(1f)
+                                    }
+                                } else if (isHeadsUpOrAnimatingAway) {
                                     // Ensure HUNs will be visible in QS shade (at least while
                                     // unlocked)
                                     emit(1f)
                                 } else if (shadeExpansion > 0f || qsExpansion > 0f) {
-                                    // Fade out as QS shade expands
+                                    // On a narrow screen, the QS shade overlaps with lockscreen
+                                    // notifications. Fade them out as the QS shade expands.
                                     emit(1f - qsExpansion)
                                 }
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index a339bc9..fa4fe46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -47,6 +47,7 @@
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.core.NewStatusBarIcons;
 import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
 import com.android.systemui.statusbar.phone.ui.TintedIconManager;
@@ -61,6 +62,7 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Objects;
 
 /**
  * The header group on Keyguard.
@@ -78,7 +80,7 @@
 
     private TextView mCarrierLabel;
     private ImageView mMultiUserAvatar;
-    private BatteryMeterView mBatteryView;
+    @Nullable private BatteryMeterView mBatteryView;
     private StatusIconContainer mStatusIconContainer;
     private StatusBarUserSwitcherContainer mUserSwitcherContainer;
 
@@ -103,6 +105,9 @@
      */
     private int mCutoutSideNudge = 0;
 
+    @Nullable
+    private WindowInsets mPreviousInsets = null;
+
     private DisplayCutout mDisplayCutout;
     private int mRoundedCornerPadding = 0;
     // right and left padding applied to this view to account for cutouts and rounded corners
@@ -127,6 +132,11 @@
         mMultiUserAvatar = findViewById(R.id.multi_user_avatar);
         mCarrierLabel = findViewById(R.id.keyguard_carrier_text);
         mBatteryView = mSystemIconsContainer.findViewById(R.id.battery);
+        if (NewStatusBarIcons.isEnabled()) {
+            // When this flag is rolled forward, this whole view can be removed
+            mBatteryView.setVisibility(View.GONE);
+            mBatteryView = null;
+        }
         mCutoutSpace = findViewById(R.id.cutout_space_view);
         mStatusIconArea = findViewById(R.id.status_icon_area);
         mStatusIconContainer = findViewById(R.id.statusIcons);
@@ -255,7 +265,10 @@
                 mMultiUserAvatar.setVisibility(View.GONE);
             }
         }
-        mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable);
+
+        if (mBatteryView != null) {
+            mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable);
+        }
     }
 
     private void updateSystemIconsLayoutParams() {
@@ -284,9 +297,12 @@
     WindowInsets updateWindowInsets(
             WindowInsets insets,
             StatusBarContentInsetsProvider insetsProvider) {
-        mLayoutState = LAYOUT_NONE;
-        if (updateLayoutConsideringCutout(insetsProvider)) {
-            requestLayout();
+        if (!Objects.equals(mPreviousInsets, insets)) {
+            mLayoutState = LAYOUT_NONE;
+            if (updateLayoutConsideringCutout(insetsProvider)) {
+                requestLayout();
+            }
+            mPreviousInsets = new WindowInsets(insets);
         }
         return super.onApplyWindowInsets(insets);
     }
@@ -435,7 +451,9 @@
 
     /** Should only be called from {@link KeyguardStatusBarViewController}. */
     void onThemeChanged(TintedIconManager iconManager) {
-        mBatteryView.setColorsFromContext(mContext);
+        if (mBatteryView != null) {
+            mBatteryView.setColorsFromContext(mContext);
+        }
         updateIconsAndTextColors(iconManager);
     }
 
@@ -443,7 +461,9 @@
     void onOverlayChanged() {
         final int carrierTheme = R.style.TextAppearance_StatusBar_Clock;
         mCarrierLabel.setTextAppearance(carrierTheme);
-        mBatteryView.updatePercentView();
+        if (mBatteryView != null) {
+            mBatteryView.updatePercentView();
+        }
 
         final int userSwitcherTheme = R.style.TextAppearance_StatusBar_UserChip;
         TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 40245ae..de72154 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -34,10 +34,12 @@
 import android.util.MathUtils;
 import android.view.DisplayCutout;
 import android.view.View;
+import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.compose.ui.platform.ComposeView;
 import androidx.core.animation.Animator;
 import androidx.core.animation.AnimatorListenerAdapter;
 import androidx.core.animation.ValueAnimator;
@@ -62,6 +64,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.core.NewStatusBarIcons;
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore;
 import com.android.systemui.statusbar.disableflags.DisableStateTracker;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
@@ -71,10 +74,13 @@
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor;
 import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt;
 import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventDefaultAnimator;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
 import com.android.systemui.statusbar.phone.ui.TintedIconManager;
+import com.android.systemui.statusbar.pipeline.battery.ui.binder.UnifiedBatteryViewBinder;
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -125,6 +131,7 @@
     private final StatusBarIconController mStatusBarIconController;
     private final TintedIconManager.Factory mTintedIconManagerFactory;
     private final BatteryMeterViewController mBatteryMeterViewController;
+    private final BatteryViewModel.Factory mBatteryViewModelFactory;
     private final ShadeViewStateProvider mShadeViewStateProvider;
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardBypassController mKeyguardBypassController;
@@ -145,7 +152,7 @@
     private final GlanceableHubToLockscreenTransitionViewModel mHubToLockscreenTransitionViewModel;
     private final LockscreenToGlanceableHubTransitionViewModel mLockscreenToHubTransitionViewModel;
 
-    private View mSystemIconsContainer;
+    private ViewGroup mSystemIconsContainer;
     private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
@@ -327,6 +334,7 @@
             StatusBarIconController statusBarIconController,
             TintedIconManager.Factory tintedIconManagerFactory,
             BatteryMeterViewController batteryMeterViewController,
+            BatteryViewModel.Factory batteryViewModelFactory,
             ShadeViewStateProvider shadeViewStateProvider,
             KeyguardStateController keyguardStateController,
             KeyguardBypassController bypassController,
@@ -360,6 +368,7 @@
         mStatusBarIconController = statusBarIconController;
         mTintedIconManagerFactory = tintedIconManagerFactory;
         mBatteryMeterViewController = batteryMeterViewController;
+        mBatteryViewModelFactory = batteryViewModelFactory;
         mShadeViewStateProvider = shadeViewStateProvider;
         mKeyguardStateController = keyguardStateController;
         mKeyguardBypassController = bypassController;
@@ -417,7 +426,9 @@
     protected void onInit() {
         super.onInit();
         mCarrierTextController.init();
-        mBatteryMeterViewController.init();
+        if (!NewStatusBarIcons.isEnabled()) {
+            mBatteryMeterViewController.init();
+        }
         if (isMigrationEnabled()) {
             KeyguardStatusBarViewBinder.bind(mView, mKeyguardStatusBarViewModel);
         }
@@ -469,6 +480,15 @@
                 mToGlanceableHubStatusBarAlphaConsumer, mCoroutineDispatcher);
         collectFlow(mView, mHubToLockscreenTransitionViewModel.getStatusBarAlpha(),
                 mFromGlanceableHubStatusBarAlphaConsumer, mCoroutineDispatcher);
+        if (NewStatusBarIcons.isEnabled()) {
+            ComposeView batteryComposeView = new ComposeView(mContext);
+            UnifiedBatteryViewBinder.bind(
+                    batteryComposeView,
+                    mBatteryViewModelFactory,
+                    DarkIconInteractor.toIsAreaDark(mView.darkChangeFlow()));
+
+            mSystemIconsContainer.addView(batteryComposeView, -1);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 4d222fd..b2d3377 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -81,6 +81,9 @@
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.wakelock.DelayedWakeLock;
 import com.android.systemui.util.wakelock.WakeLock;
+import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor;
+
+import dagger.Lazy;
 
 import kotlinx.coroutines.CoroutineDispatcher;
 
@@ -226,7 +229,7 @@
     private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA;
 
     static final float TRANSPARENT_BOUNCER_SCRIM_ALPHA = 0.54f;
-    private final float mDefaultScrimAlpha;
+    private float mDefaultScrimAlpha;
 
     private float mRawPanelExpansionFraction;
     private float mPanelScrimMinFraction;
@@ -257,6 +260,7 @@
     private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener;
     private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
     private final BlurConfig mBlurConfig;
+    private final Lazy<WindowRootViewBlurInteractor> mWindowRootViewBlurInteractor;
     private Consumer<Integer> mScrimVisibleListener;
     private boolean mBlankScreen;
     private boolean mScreenBlankingCallbackCalled;
@@ -339,14 +343,13 @@
             KeyguardInteractor keyguardInteractor,
             @Main CoroutineDispatcher mainDispatcher,
             LargeScreenShadeInterpolator largeScreenShadeInterpolator,
-            BlurConfig blurConfig) {
+            BlurConfig blurConfig,
+            Lazy<WindowRootViewBlurInteractor> windowRootViewBlurInteractor) {
         mScrimStateListener = lightBarController::setScrimState;
         mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
         mBlurConfig = blurConfig;
-        // All scrims default alpha need to match bouncer background alpha to make sure the
-        // transitions involving the bouncer are smooth and don't overshoot the bouncer alpha.
-        mDefaultScrimAlpha =
-                Flags.bouncerUiRevamp() ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : BUSY_SCRIM_ALPHA;
+        mWindowRootViewBlurInteractor = windowRootViewBlurInteractor;
+        mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
 
         mKeyguardStateController = keyguardStateController;
         mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
@@ -407,7 +410,7 @@
 
         final ScrimState[] states = ScrimState.values();
         for (int i = 0; i < states.length; i++) {
-            states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, mBlurConfig);
+            states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager);
             states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard);
             states[i].setDefaultScrimAlpha(mDefaultScrimAlpha);
         }
@@ -485,6 +488,30 @@
                         Edge.Companion.create(Scenes.Communal, LOCKSCREEN),
                         Edge.Companion.create(GLANCEABLE_HUB, LOCKSCREEN)),
                 mGlanceableHubConsumer, mMainDispatcher);
+
+        if (Flags.bouncerUiRevamp()) {
+            collectFlow(behindScrim,
+                    mWindowRootViewBlurInteractor.get().isBlurCurrentlySupported(),
+                    this::handleBlurSupportedChanged);
+        }
+    }
+
+    private void updateDefaultScrimAlpha(float alpha) {
+        mDefaultScrimAlpha = alpha;
+        for (ScrimState state : ScrimState.values()) {
+            state.setDefaultScrimAlpha(mDefaultScrimAlpha);
+        }
+        applyAndDispatchState();
+    }
+
+    private void handleBlurSupportedChanged(boolean isBlurSupported) {
+        if (isBlurSupported) {
+            updateDefaultScrimAlpha(TRANSPARENT_BOUNCER_SCRIM_ALPHA);
+            ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx());
+        } else {
+            ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(0f);
+            updateDefaultScrimAlpha(BUSY_SCRIM_ALPHA);
+        }
     }
 
     // TODO(b/270984686) recompute scrim height accurately, based on shade contents.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 5f423cf..071a57a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -17,14 +17,12 @@
 package com.android.systemui.statusbar.phone;
 
 import static com.android.systemui.statusbar.phone.ScrimController.BUSY_SCRIM_ALPHA;
-import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT_BOUNCER_SCRIM_ALPHA;
 
 import android.graphics.Color;
 
 import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.systemui.Flags;
 import com.android.systemui.dock.DockManager;
-import com.android.systemui.keyguard.ui.transitions.BlurConfig;
 import com.android.systemui.res.R;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shade.ui.ShadeColors;
@@ -116,8 +114,8 @@
         @Override
         public void prepare(ScrimState previousState) {
             if (Flags.bouncerUiRevamp()) {
-                mBehindAlpha = mClipQsScrim ? 0.0f : TRANSPARENT_BOUNCER_SCRIM_ALPHA;
-                mNotifAlpha = mClipQsScrim ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : 0;
+                mBehindAlpha = mDefaultScrimAlpha;
+                mNotifAlpha = 0f;
                 mBehindTint = mNotifTint = mSurfaceColor;
                 mFrontAlpha = 0f;
                 return;
@@ -153,12 +151,11 @@
                 if (previousState == SHADE_LOCKED) {
                     mBehindAlpha = previousState.getBehindAlpha();
                     mNotifAlpha = previousState.getNotifAlpha();
-                    mNotifBlurRadius = mBlurConfig.getMaxBlurRadiusPx();
                 } else {
                     mNotifAlpha = 0f;
                     mBehindAlpha = 0f;
                 }
-                mFrontAlpha = TRANSPARENT_BOUNCER_SCRIM_ALPHA;
+                mFrontAlpha = mDefaultScrimAlpha;
                 mFrontTint = mSurfaceColor;
                 return;
             }
@@ -403,7 +400,6 @@
     DozeParameters mDozeParameters;
     DockManager mDockManager;
     boolean mDisplayRequiresBlanking;
-    protected BlurConfig mBlurConfig;
     boolean mLaunchingAffordanceWithPreview;
     boolean mOccludeAnimationPlaying;
     boolean mWakeLockScreenSensorActive;
@@ -417,7 +413,7 @@
     protected float mNotifBlurRadius = 0.0f;
 
     public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters,
-            DockManager dockManager, BlurConfig blurConfig) {
+            DockManager dockManager) {
         mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark);
         mScrimInFront = scrimInFront;
         mScrimBehind = scrimBehind;
@@ -425,7 +421,6 @@
         mDozeParameters = dozeParameters;
         mDockManager = dockManager;
         mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking();
-        mBlurConfig = blurConfig;
     }
 
     /** Prepare state for transition. */
@@ -536,4 +531,8 @@
     public float getNotifBlurRadius() {
         return mNotifBlurRadius;
     }
+
+    public void setNotifBlurRadius(float value) {
+        mNotifBlurRadius = value;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index b2c4ef9..01de925 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -65,6 +65,7 @@
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
 import com.android.systemui.bouncer.ui.BouncerView;
 import com.android.systemui.bouncer.util.BouncerTestUtilsKt;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
@@ -170,6 +171,7 @@
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
     private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
     private final DismissCallbackRegistry mDismissCallbackRegistry;
+    private final CommunalSceneInteractor mCommunalSceneInteractor;
 
     private Job mListenForAlternateBouncerTransitionSteps = null;
     private Job mListenForKeyguardAuthenticatedBiometricsHandled = null;
@@ -406,7 +408,8 @@
             @Main DelayableExecutor executor,
             Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
             DismissCallbackRegistry dismissCallbackRegistry,
-            Lazy<BouncerInteractor> bouncerInteractor
+            Lazy<BouncerInteractor> bouncerInteractor,
+            CommunalSceneInteractor communalSceneInteractor
     ) {
         mContext = context;
         mExecutor = executor;
@@ -443,6 +446,7 @@
         mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
         mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
         mDismissCallbackRegistry = dismissCallbackRegistry;
+        mCommunalSceneInteractor = communalSceneInteractor;
     }
 
     KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -1364,11 +1368,13 @@
         }
         mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
 
-        boolean hideBouncerOverDream = isBouncerShowing()
-                && mDreamOverlayStateController.isOverlayActive();
+        boolean hideBouncerOverDreamOrHub = isBouncerShowing()
+                && (mDreamOverlayStateController.isOverlayActive()
+                || mCommunalSceneInteractor.isIdleOnCommunal().getValue());
         mCentralSurfaces.endAffordanceLaunch();
         // The second condition is for SIM card locked bouncer
-        if (hideBouncerOverDream || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
+        if (hideBouncerOverDreamOrHub
+                || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
             hideBouncer(false);
             updateStates();
         } else {
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 e33baf7..ded964d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -57,11 +57,11 @@
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeDisplayAware;
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
 import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
 import com.android.systemui.statusbar.CommandQueue;
@@ -76,11 +76,11 @@
 import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.wmshell.BubblesManager;
 
@@ -115,7 +115,6 @@
     private final static String TAG = "StatusBarNotificationActivityStarter";
 
     private final Context mContext;
-    private final int mDisplayId;
 
     private final Handler mMainThreadHandler;
     private final Executor mUiBgExecutor;
@@ -155,8 +154,7 @@
 
     @Inject
     StatusBarNotificationActivityStarter(
-            Context context,
-            @DisplayId int displayId,
+            @ShadeDisplayAware Context context,
             Handler mainThreadHandler,
             @Background Executor uiBgExecutor,
             NotificationVisibilityProvider visibilityProvider,
@@ -189,7 +187,6 @@
             PowerInteractor powerInteractor,
             UserTracker userTracker) {
         mContext = context;
-        mDisplayId = displayId;
         mMainThreadHandler = mainThreadHandler;
         mUiBgExecutor = uiBgExecutor;
         mVisibilityProvider = visibilityProvider;
@@ -493,6 +490,7 @@
             boolean animate,
             boolean isActivityIntent) {
         mLogger.logStartNotificationIntent(entry);
+        final int displayId = mContext.getDisplayId();
         try {
             ActivityTransitionAnimator.Controller animationController =
                     new StatusBarTransitionAnimatorController(
@@ -501,7 +499,7 @@
                             mShadeController,
                             mNotificationShadeWindowController,
                             mCommandQueue,
-                            mDisplayId,
+                            displayId,
                             isActivityIntent);
             mActivityTransitionAnimator.startPendingIntentWithAnimation(
                     animationController,
@@ -511,11 +509,11 @@
                         long eventTime = row.getAndResetLastActionUpTime();
                         Bundle options = eventTime > 0
                                 ? getActivityOptions(
-                                mDisplayId,
+                                displayId,
                                 adapter,
                                 mKeyguardStateController.isShowing(),
                                 eventTime)
-                                : getActivityOptions(mDisplayId, adapter);
+                                : getActivityOptions(displayId, adapter);
                         int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
                                 null, null, options);
                         mLogger.logSendPendingIntent(entry, intent, result);
@@ -533,6 +531,7 @@
     public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid,
             @NonNull ExpandableNotificationRow row) {
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
+        final int displayId = mContext.getDisplayId();
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
             public boolean onDismiss() {
@@ -544,7 +543,7 @@
                                     mShadeController,
                                     mNotificationShadeWindowController,
                                     mCommandQueue,
-                                    mDisplayId,
+                                    displayId,
                                     true /* isActivityIntent */);
 
                     mActivityTransitionAnimator.startIntentWithAnimation(
@@ -552,7 +551,7 @@
                             (adapter) -> TaskStackBuilder.create(mContext)
                                     .addNextIntentWithParentStack(intent)
                                     .startActivities(getActivityOptions(
-                                                    mDisplayId,
+                                                    displayId,
                                                     adapter),
                                             new UserHandle(UserHandle.getUserId(appUid))));
                 });
@@ -571,6 +570,7 @@
     @Override
     public void startHistoryIntent(View view, boolean showHistory) {
         ModesEmptyShadeFix.assertInLegacyMode();
+        final int displayId = mContext.getDisplayId();
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
@@ -597,13 +597,13 @@
                                             mShadeController,
                                             mNotificationShadeWindowController,
                                             mCommandQueue,
-                                            mDisplayId,
+                                            displayId,
                                             true /* isActivityIntent */);
 
                     mActivityTransitionAnimator.startIntentWithAnimation(
                             animationController, animate, intent.getPackage(),
                             (adapter) -> tsb.startActivities(
-                                    getActivityOptions(mDisplayId, adapter),
+                                    getActivityOptions(displayId, adapter),
                                     mUserTracker.getUserHandle()));
                 });
                 return true;
@@ -620,6 +620,7 @@
 
     @Override
     public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) {
+        final int displayId = mContext.getDisplayId();
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
@@ -642,13 +643,13 @@
                                             mShadeController,
                                             mNotificationShadeWindowController,
                                             mCommandQueue,
-                                            mDisplayId,
+                                            displayId,
                                             true /* isActivityIntent */);
 
                     mActivityTransitionAnimator.startIntentWithAnimation(
                             animationController, animate, intentInfo.getTargetIntent().getPackage(),
                             (adapter) -> tsb.startActivities(
-                                    getActivityOptions(mDisplayId, adapter),
+                                    getActivityOptions(displayId, adapter),
                                     mUserTracker.getUserHandle()));
                 });
                 return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
index 7207d0a..4d531b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.qualifiers.DisplaySpecific;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.statusbar.core.NewStatusBarIcons;
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
 import com.android.systemui.statusbar.layout.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
@@ -85,7 +86,9 @@
     default void init() {
         // No one accesses these controllers, so we need to make sure we reference them here so they
         // do get initialized.
-        getBatteryMeterViewController().init();
+        if (!NewStatusBarIcons.isEnabled()) {
+            getBatteryMeterViewController().init();
+        }
         getHeadsUpAppearanceController().init();
         getPhoneStatusBarViewController().init();
         if (!NotificationsLiveDataStoreRefactor.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt
index 903844e..9665c33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt
@@ -16,7 +16,10 @@
 
 package com.android.systemui.statusbar.pipeline.battery.ui.binder
 
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.core.view.isVisible
@@ -27,6 +30,8 @@
 import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
 import com.android.systemui.statusbar.pipeline.battery.ui.composable.UnifiedBattery
 import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_HEIGHT
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_WIDTH
 import kotlinx.coroutines.flow.Flow
 
 /** In cases where the battery needs to be bound to an existing android view */
@@ -47,7 +52,13 @@
                     )
                     setContent {
                         val isDark by isAreaDark.collectAsStateWithLifecycle(IsAreaDark { true })
-                        UnifiedBattery(viewModelFactory = viewModelFactory, isDark = isDark)
+                        UnifiedBattery(
+                            modifier =
+                                Modifier.height(STATUS_BAR_BATTERY_HEIGHT)
+                                    .width(STATUS_BAR_BATTERY_WIDTH),
+                            viewModelFactory = viewModelFactory,
+                            isDark = isDark,
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt
index 2ee86ee..ac793a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt
@@ -54,7 +54,7 @@
         )
         if (showEstimate) {
             viewModel.batteryTimeRemainingEstimate?.let {
-                Spacer(modifier.width(2.dp))
+                Spacer(modifier.width(4.dp))
                 Text(text = it, color = Color.White)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt
index d0d099e7..afd4bb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.unit.dp
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -223,6 +224,10 @@
     }
 
     companion object {
+        // Status bar battery height, based on a 21x12 base canvas
+        val STATUS_BAR_BATTERY_HEIGHT = 13.dp
+        val STATUS_BAR_BATTERY_WIDTH = 22.75.dp
+
         fun Int.glyphRepresentation(): List<BatteryGlyph> = toString().map { it.toGlyph() }
 
         private fun Char.toGlyph(): BatteryGlyph =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index 9d72daf..c34fa46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -23,6 +23,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
@@ -34,9 +36,11 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.theme.PlatformTheme
 import com.android.keyguard.AlphaOptimizedLinearLayout
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips
+import com.android.systemui.statusbar.core.NewStatusBarIcons
 import com.android.systemui.statusbar.core.StatusBarRootModernization
 import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore
 import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
@@ -51,6 +55,9 @@
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ui.DarkIconManager
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.pipeline.battery.ui.composable.UnifiedBattery
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_HEIGHT
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_WIDTH
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIconBlockListBinder
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder
 import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel
@@ -73,14 +80,13 @@
 ) {
     fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView {
         val composeView = ComposeView(root.context)
-        val displayId = root.context.displayId
         val darkIconDispatcher =
             darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView
         composeView.apply {
             setContent {
                 StatusBarRoot(
                     parent = root,
-                    statusBarViewModel = homeStatusBarViewModelFactory.create(displayId),
+                    statusBarViewModelFactory = homeStatusBarViewModelFactory,
                     statusBarViewBinder = homeStatusBarViewBinder,
                     notificationIconsBinder = notificationIconsBinder,
                     darkIconManagerFactory = darkIconManagerFactory,
@@ -110,7 +116,7 @@
 @Composable
 fun StatusBarRoot(
     parent: ViewGroup,
-    statusBarViewModel: HomeStatusBarViewModel,
+    statusBarViewModelFactory: HomeStatusBarViewModelFactory,
     statusBarViewBinder: HomeStatusBarViewBinder,
     notificationIconsBinder: NotificationIconContainerStatusBarViewBinder,
     darkIconManagerFactory: DarkIconManager.Factory,
@@ -120,6 +126,10 @@
     eventAnimationInteractor: SystemStatusEventAnimationInteractor,
     onViewCreated: (ViewGroup) -> Unit,
 ) {
+    val displayId = parent.context.displayId
+    val statusBarViewModel =
+        rememberViewModel("HomeStatusBar") { statusBarViewModelFactory.create(displayId) }
+
     Box(Modifier.fillMaxSize()) {
         // TODO(b/364360986): remove this before rolling the flag forward
         if (StatusBarRootModernization.SHOW_DISAMBIGUATION) {
@@ -159,10 +169,6 @@
                                         LinearLayout.LayoutParams.WRAP_CONTENT,
                                     )
 
-                                setViewCompositionStrategy(
-                                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
-                                )
-
                                 setContent {
                                     PlatformTheme {
                                         val chips by
@@ -241,6 +247,12 @@
                         endSideContent.addView(composeView, 0)
                     }
 
+                    // If the flag is enabled, create and add a compose battery view to the end
+                    // of the system_icons container
+                    if (NewStatusBarIcons.isEnabled) {
+                        addBatteryComposable(phoneStatusBarView, statusBarViewModel)
+                    }
+
                     notificationIconsBinder.bindWhileAttached(
                         notificationIconContainer,
                         context.displayId,
@@ -263,6 +275,27 @@
     }
 }
 
+/** Create a new [UnifiedBattery] and add it to the end of the system_icons container */
+private fun addBatteryComposable(
+    phoneStatusBarView: PhoneStatusBarView,
+    statusBarViewModel: HomeStatusBarViewModel,
+) {
+    val batteryComposeView =
+        ComposeView(phoneStatusBarView.context).apply {
+            setContent {
+                UnifiedBattery(
+                    modifier =
+                        Modifier.height(STATUS_BAR_BATTERY_HEIGHT).width(STATUS_BAR_BATTERY_WIDTH),
+                    viewModelFactory = statusBarViewModel.batteryViewModelFactory,
+                    isDark = statusBarViewModel.areaDark,
+                )
+            }
+        }
+    phoneStatusBarView.findViewById<ViewGroup>(R.id.system_icons).apply {
+        addView(batteryComposeView, -1)
+    }
+}
+
 /**
  * This is our analog of the flexi "ribbon", which just shows some text so we know if the flag is on
  */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index 1bc45a9..f396cbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -19,6 +19,7 @@
 import android.annotation.ColorInt
 import android.graphics.Rect
 import android.view.View
+import androidx.compose.runtime.getValue
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -28,6 +29,8 @@
 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.TransitionState
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.plugins.DarkIconDispatcher
@@ -55,8 +58,10 @@
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor
+import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
 import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor
 import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel
@@ -90,6 +95,9 @@
  * so that it's all in one place and easily testable outside of the fragment.
  */
 interface HomeStatusBarViewModel {
+    /** Factory to create the view model for the battery icon */
+    val batteryViewModelFactory: BatteryViewModel.Factory
+
     /**
      * True if the device is currently transitioning from lockscreen to occluded and false
      * otherwise.
@@ -171,6 +179,9 @@
      */
     val areaTint: Flow<StatusBarTintColor>
 
+    /** [IsAreaDark] applicable for this status bar's display and content area */
+    val areaDark: IsAreaDark
+
     /** Interface for the assisted factory, to allow for providing a fake in tests */
     interface HomeStatusBarViewModelFactory {
         fun create(displayId: Int): HomeStatusBarViewModel
@@ -181,6 +192,7 @@
 @AssistedInject
 constructor(
     @Assisted thisDisplayId: Int,
+    override val batteryViewModelFactory: BatteryViewModel.Factory,
     tableLoggerFactory: TableLogBufferFactory,
     homeStatusBarInteractor: HomeStatusBarInteractor,
     homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor,
@@ -201,7 +213,9 @@
     statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
     @Background bgScope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
-) : HomeStatusBarViewModel {
+) : HomeStatusBarViewModel, ExclusiveActivatable() {
+
+    private val hydrator = Hydrator(traceName = "HomeStatusBarViewModel.hydrator")
 
     val tableLogger = tableLoggerFactory.getOrCreate(tableLogBufferName(thisDisplayId), 200)
 
@@ -294,6 +308,13 @@
             .distinctUntilChanged()
             .flowOn(bgDispatcher)
 
+    override val areaDark: IsAreaDark by
+        hydrator.hydratedStateOf(
+            traceName = "areaDark",
+            initialValue = IsAreaDark { true },
+            source = darkIconInteractor.isAreaDark(thisDisplayId),
+        )
+
     /**
      * True if the current SysUI state can show the home status bar (aka this status bar), and false
      * if we shouldn't be showing any part of the home status bar.
@@ -473,6 +494,10 @@
     @View.Visibility
     private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE
 
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
+
     /** Inject this to create the display-dependent view model */
     @AssistedFactory
     interface HomeStatusBarViewModelFactoryImpl :
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
index 72d093c..9f05850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
@@ -22,11 +22,11 @@
 
     /** Returns true if the device should use the split notification shade. */
     @Deprecated(
-        message = "This is deprecated, please use ShadeInteractor#shadeMode instead",
+        message = "This is deprecated, please use ShadeModeInteractor#shadeMode instead",
         replaceWith =
             ReplaceWith(
-                "shadeInteractor.shadeMode",
-                "com.android.systemui.shade.domain.interactor.ShadeInteractor",
+                "shadeModeInteractor.shadeMode",
+                "com.android.systemui.shade.domain.interactor.ShadeModeInteractor",
             ),
     )
     fun shouldUseSplitNotificationShade(resources: Resources): Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
index 0ec996b..9b4902a 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
@@ -6,5 +6,4 @@
 mgalhardo@google.com
 petrcermak@google.com
 stevenckng@google.com
-tkachenkoi@google.com
-vanjan@google.com
\ No newline at end of file
+vanjan@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
index f5aac72..e1640cd 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
@@ -19,8 +19,11 @@
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.app.tracing.TraceUtils.traceAsync
 import com.android.app.tracing.instantForTrack
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -30,10 +33,12 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
 import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import com.android.systemui.util.Compile
 import com.android.systemui.util.Utils.isDeviceFoldable
@@ -42,17 +47,23 @@
 import com.android.systemui.util.kotlin.race
 import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.time.measureTimeMillis
-import java.time.Duration
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.timeout
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.withTimeout
 
 /**
@@ -73,63 +84,96 @@
     @Application private val applicationScope: CoroutineScope,
     private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger,
     private val systemClock: SystemClock,
-    private val deviceStateManager: DeviceStateManager
+    private val deviceStateManager: DeviceStateManager,
+    private val latencyTracker: LatencyTracker,
 ) : CoreStartable {
 
     private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
     private val isAodEnabled: Boolean
         get() = keyguardInteractor.isAodAvailable.value
 
+    private val displaySwitchStarted =
+        deviceStateRepository.state.pairwise().filter {
+            // Start tracking only when the foldable device is
+            // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
+            foldableDeviceState ->
+            foldableDeviceState.previousValue == DeviceState.FOLDED ||
+                foldableDeviceState.newValue == DeviceState.FOLDED
+        }
+
+    private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow())
+
+    private var isCoolingDown = false
+
     override fun start() {
         if (!isDeviceFoldable(context.resources, deviceStateManager)) {
             return
         }
         applicationScope.launch(context = backgroundDispatcher) {
-            deviceStateRepository.state
-                .pairwise()
-                .filter {
-                    // Start tracking only when the foldable device is
-                    // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
-                    // unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
-                    foldableDeviceState ->
-                    foldableDeviceState.previousValue == DeviceState.FOLDED ||
-                        foldableDeviceState.newValue == DeviceState.FOLDED
+            displaySwitchStarted.collectLatest { (previousState, newState) ->
+                if (isCoolingDown) return@collectLatest
+                if (previousState == DeviceState.FOLDED) {
+                    latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+                    instantForTrack(TAG) { "unfold latency tracking started" }
                 }
-                .flatMapLatest { foldableDeviceState ->
-                    flow {
-                        var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent()
-                        val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt()
-                        displaySwitchLatencyEvent =
-                            displaySwitchLatencyEvent.withBeforeFields(
-                                foldableDeviceState.previousValue.toStatsInt()
-                            )
-
+                try {
+                    withTimeout(SCREEN_EVENT_TIMEOUT) {
+                        val event =
+                            DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt())
                         val displaySwitchTimeMs =
                             measureTimeMillis(systemClock) {
-                                try {
-                                    withTimeout(SCREEN_EVENT_TIMEOUT) {
-                                        traceAsync(TAG, "displaySwitch") {
-                                            waitForDisplaySwitch(toFoldableDeviceState)
-                                        }
-                                    }
-                                } catch (e: TimeoutCancellationException) {
-                                    Log.e(TAG, "Wait for display switch timed out")
+                                traceAsync(TAG, "displaySwitch") {
+                                    waitForDisplaySwitch(newState.toStatsInt())
                                 }
                             }
-
-                        displaySwitchLatencyEvent =
-                            displaySwitchLatencyEvent.withAfterFields(
-                                toFoldableDeviceState,
-                                displaySwitchTimeMs.toInt(),
-                                getCurrentState()
-                            )
-                        emit(displaySwitchLatencyEvent)
+                        if (previousState == DeviceState.FOLDED) {
+                            latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+                        }
+                        logDisplaySwitchEvent(event, newState, displaySwitchTimeMs)
                     }
+                } catch (e: TimeoutCancellationException) {
+                    instantForTrack(TAG) { "tracking timed out" }
+                    latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+                } catch (e: CancellationException) {
+                    instantForTrack(TAG) { "new state interrupted, entering cool down" }
+                    latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+                    startCoolDown()
                 }
-                .collect { displaySwitchLatencyLogger.log(it) }
+            }
         }
     }
 
+    @OptIn(FlowPreview::class)
+    private fun startCoolDown() {
+        if (isCoolingDown) return
+        isCoolingDown = true
+        applicationScope.launch(context = backgroundDispatcher) {
+            val startTime = systemClock.elapsedRealtime()
+            try {
+                startOrEndEvent.timeout(COOL_DOWN_DURATION).collect()
+            } catch (e: TimeoutCancellationException) {
+                instantForTrack(TAG) {
+                    "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms"
+                }
+                isCoolingDown = false
+            }
+        }
+    }
+
+    private fun logDisplaySwitchEvent(
+        event: DisplaySwitchLatencyEvent,
+        toFoldableDeviceState: DeviceState,
+        displaySwitchTimeMs: Long,
+    ) {
+        displaySwitchLatencyLogger.log(
+            event.withAfterFields(
+                toFoldableDeviceState.toStatsInt(),
+                displaySwitchTimeMs.toInt(),
+                getCurrentState(),
+            )
+        )
+    }
+
     private fun DeviceState.toStatsInt(): Int =
         when (this) {
             DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED
@@ -152,25 +196,42 @@
         }
     }
 
+    private fun anyEndEventFlow(): Flow<Any> {
+        val unfoldStatus =
+            unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted }
+        // dropping first emission as we're only interested in new emissions, not current state
+        val screenOn =
+            powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON }
+        val goToSleep =
+            powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) }
+        return merge(screenOn, goToSleep, unfoldStatus)
+    }
+
     private fun shouldWaitForTransitionStart(
         toFoldableDeviceState: Int,
-        isTransitionEnabled: Boolean
+        isTransitionEnabled: Boolean,
     ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled)
 
     private suspend fun waitForScreenTurnedOn() {
         traceAsync(TAG, "waitForScreenTurnedOn()") {
-            powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
+            // dropping first as it's stateFlow and will always emit latest value but we're
+            // only interested in new states
+            powerInteractor.screenPowerState
+                .drop(1)
+                .filter { it == ScreenPowerState.SCREEN_ON }
+                .first()
         }
     }
 
     private suspend fun waitForGoToSleepWithScreenOff() {
         traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
-            powerInteractor.detailedWakefulness
-                .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled }
-                .first()
+            powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first()
         }
     }
 
+    private fun sleepWithScreenOff(model: WakefulnessModel) =
+        model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled
+
     private fun getCurrentState(): Int =
         when {
             isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD
@@ -205,7 +266,7 @@
     private fun DisplaySwitchLatencyEvent.withAfterFields(
         toFoldableDeviceState: Int,
         displaySwitchTimeMs: Int,
-        toState: Int
+        toState: Int,
     ): DisplaySwitchLatencyEvent {
         log {
             "toFoldableDeviceState=$toFoldableDeviceState, " +
@@ -217,7 +278,7 @@
         return copy(
             toFoldableDeviceState = toFoldableDeviceState,
             latencyMs = displaySwitchTimeMs,
-            toState = toState
+            toState = toState,
         )
     }
 
@@ -250,14 +311,15 @@
         val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN,
         val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN,
         val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN,
-        val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN
+        val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN,
     )
 
     companion object {
         private const val VALUE_UNKNOWN = -1
         private const val TAG = "DisplaySwitchLatency"
         private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
-        private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis()
+        @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds
+        @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds
 
         private const val FOLDABLE_DEVICE_STATE_UNKNOWN =
             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt
new file mode 100644
index 0000000..91f1426
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.systemui.unfold
+
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import android.util.Log
+import com.android.app.tracing.TraceUtils.traceAsync
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.app.tracing.instantForTrack
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.util.Compile
+import com.android.systemui.util.Utils.isDeviceFoldable
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.race
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.measureTimeMillis
+import java.time.Duration
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which
+ * version is used for tracking depends on [unfoldLatencyTrackingFix] flag.
+ */
+@SysUISingleton
+class NoCooldownDisplaySwitchLatencyTracker
+@Inject
+constructor(
+    private val context: Context,
+    private val deviceStateRepository: DeviceStateRepository,
+    private val powerInteractor: PowerInteractor,
+    private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
+    private val animationStatusRepository: AnimationStatusRepository,
+    private val keyguardInteractor: KeyguardInteractor,
+    @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor,
+    @Application private val applicationScope: CoroutineScope,
+    private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger,
+    private val systemClock: SystemClock,
+    private val deviceStateManager: DeviceStateManager,
+) : CoreStartable {
+
+    private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
+    private val isAodEnabled: Boolean
+        get() = keyguardInteractor.isAodAvailable.value
+
+    override fun start() {
+        if (!isDeviceFoldable(context.resources, deviceStateManager)) {
+            return
+        }
+        applicationScope.launch(context = backgroundDispatcher) {
+            deviceStateRepository.state
+                .pairwise()
+                .filter {
+                    // Start tracking only when the foldable device is
+                    // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
+                    // unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
+                    foldableDeviceState ->
+                    foldableDeviceState.previousValue == DeviceState.FOLDED ||
+                        foldableDeviceState.newValue == DeviceState.FOLDED
+                }
+                .flatMapLatest { foldableDeviceState ->
+                    flow {
+                        var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent()
+                        val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt()
+                        displaySwitchLatencyEvent =
+                            displaySwitchLatencyEvent.withBeforeFields(
+                                foldableDeviceState.previousValue.toStatsInt()
+                            )
+
+                        val displaySwitchTimeMs =
+                            measureTimeMillis(systemClock) {
+                                try {
+                                    withTimeout(SCREEN_EVENT_TIMEOUT) {
+                                        traceAsync(TAG, "displaySwitch") {
+                                            waitForDisplaySwitch(toFoldableDeviceState)
+                                        }
+                                    }
+                                } catch (e: TimeoutCancellationException) {
+                                    Log.e(TAG, "Wait for display switch timed out")
+                                }
+                            }
+
+                        displaySwitchLatencyEvent =
+                            displaySwitchLatencyEvent.withAfterFields(
+                                toFoldableDeviceState,
+                                displaySwitchTimeMs.toInt(),
+                                getCurrentState(),
+                            )
+                        emit(displaySwitchLatencyEvent)
+                    }
+                }
+                .collect { displaySwitchLatencyLogger.log(it) }
+        }
+    }
+
+    private fun DeviceState.toStatsInt(): Int =
+        when (this) {
+            DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED
+            DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN
+            DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN
+            DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED
+            else -> FOLDABLE_DEVICE_STATE_UNKNOWN
+        }
+
+    private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) {
+        val isTransitionEnabled =
+            unfoldTransitionInteractor.isAvailable &&
+                animationStatusRepository.areAnimationsEnabled().first()
+        if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) {
+            traceAsync(TAG, "waitForTransitionStart()") {
+                unfoldTransitionInteractor.waitForTransitionStart()
+            }
+        } else {
+            race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() })
+        }
+    }
+
+    private fun shouldWaitForTransitionStart(
+        toFoldableDeviceState: Int,
+        isTransitionEnabled: Boolean,
+    ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled)
+
+    private suspend fun waitForScreenTurnedOn() {
+        traceAsync(TAG, "waitForScreenTurnedOn()") {
+            // dropping first as it's stateFlow and will always emit latest value but we're
+            // only interested in new states
+            powerInteractor.screenPowerState
+                .drop(1)
+                .filter { it == ScreenPowerState.SCREEN_ON }
+                .first()
+        }
+    }
+
+    private suspend fun waitForGoToSleepWithScreenOff() {
+        traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
+            powerInteractor.detailedWakefulness
+                .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled }
+                .first()
+        }
+    }
+
+    private fun getCurrentState(): Int =
+        when {
+            isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD
+            isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF
+            else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN
+        }
+
+    private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled)
+
+    private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled)
+
+    private fun isAsleepDueToFold(): Boolean {
+        val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value
+
+        return (lastWakefulnessEvent.isAsleep() &&
+            (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD))
+    }
+
+    private inline fun log(msg: () -> String) {
+        if (DEBUG) Log.d(TAG, msg())
+    }
+
+    private fun DisplaySwitchLatencyEvent.withBeforeFields(
+        fromFoldableDeviceState: Int
+    ): DisplaySwitchLatencyEvent {
+        log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
+        instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
+
+        return copy(fromFoldableDeviceState = fromFoldableDeviceState)
+    }
+
+    private fun DisplaySwitchLatencyEvent.withAfterFields(
+        toFoldableDeviceState: Int,
+        displaySwitchTimeMs: Int,
+        toState: Int,
+    ): DisplaySwitchLatencyEvent {
+        log {
+            "toFoldableDeviceState=$toFoldableDeviceState, " +
+                "toState=$toState, " +
+                "latencyMs=$displaySwitchTimeMs"
+        }
+        instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" }
+
+        return copy(
+            toFoldableDeviceState = toFoldableDeviceState,
+            latencyMs = displaySwitchTimeMs,
+            toState = toState,
+        )
+    }
+
+    companion object {
+        private const val VALUE_UNKNOWN = -1
+        private const val TAG = "DisplaySwitchLatency"
+        private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
+        private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis()
+
+        private const val FOLDABLE_DEVICE_STATE_UNKNOWN =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN
+        const val FOLDABLE_DEVICE_STATE_CLOSED =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED
+        const val FOLDABLE_DEVICE_STATE_HALF_OPEN =
+            SysUiStatsLog
+                .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED
+        private const val FOLDABLE_DEVICE_STATE_OPEN =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED
+        private const val FOLDABLE_DEVICE_STATE_FLIPPED =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index f806a5c..9248cc8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -22,6 +22,7 @@
 import android.os.Trace
 import android.util.Log
 import com.android.internal.util.LatencyTracker
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -63,7 +64,7 @@
 
     /** Registers for relevant events only if the device is foldable. */
     fun init() {
-        if (!isFoldable) {
+        if (unfoldLatencyTrackingFix() || !isFoldable) {
             return
         }
         deviceStateManager.registerCallback(uiBgExecutor, foldStateListener)
@@ -85,7 +86,7 @@
         if (DEBUG) {
             Log.d(
                 TAG,
-                "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+                "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled",
             )
         }
 
@@ -109,7 +110,7 @@
         if (DEBUG) {
             Log.d(
                 TAG,
-                "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+                "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled",
             )
         }
 
@@ -161,7 +162,7 @@
                     Log.d(
                         TAG,
                         "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " +
-                            "isTransitionEnabled = $isTransitionEnabled"
+                            "isTransitionEnabled = $isTransitionEnabled",
                     )
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
index 885a2b0..c2f86a3 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
@@ -48,6 +49,9 @@
     val isAvailable: Boolean
         get() = repository.isAvailable
 
+    /** Flow of latest [UnfoldTransitionStatus] changes */
+    val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus
+
     /**
      * This mapping emits 1 when the device is completely unfolded and 0.0 when the device is
      * completely folded.
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
index 4b01ded..f1abf10 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
@@ -25,7 +25,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 
 /**
- * Repository for observing values of [Settings.Secure] for the currently active user. That means
+ * Repository for observing values of [Settings.System] for the currently active user. That means
  * when user is switched and the new user has different value, flow will emit new value.
  */
 // TODO: b/377244768 - Make internal once call sites inject SystemSettingsRepository instead.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 68bffeef..4d547705 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -37,8 +37,6 @@
 import android.media.IVolumeController;
 import android.media.MediaRouter2Manager;
 import android.media.VolumePolicy;
-import android.media.session.MediaController.PlaybackInfo;
-import android.media.session.MediaSession.Token;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerExecutor;
@@ -61,6 +59,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.settingslib.volume.MediaSessions;
+import com.android.settingslib.volume.MediaSessions.SessionId;
 import com.android.systemui.Dumpable;
 import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -1402,12 +1401,13 @@
     }
 
     protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks {
-        private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
+        private final HashMap<SessionId, Integer> mRemoteStreams = new HashMap<>();
 
         private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
 
         @Override
-        public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
+        public void onRemoteUpdate(
+                    SessionId token, String name, MediaSessions.VolumeInfo volumeInfo) {
                 addStream(token, "onRemoteUpdate");
 
                 int stream = 0;
@@ -1415,14 +1415,15 @@
                     stream = mRemoteStreams.get(token);
                 }
                 Slog.d(TAG,
-                        "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume());
+                        "onRemoteUpdate: stream: "
+                                + stream + " volume: " + volumeInfo.getCurrentVolume());
                 boolean changed = mState.states.indexOfKey(stream) < 0;
                 final StreamState ss = streamStateW(stream);
                 ss.dynamic = true;
                 ss.levelMin = 0;
-                ss.levelMax = pi.getMaxVolume();
-                if (ss.level != pi.getCurrentVolume()) {
-                    ss.level = pi.getCurrentVolume();
+                ss.levelMax = volumeInfo.getMaxVolume();
+                if (ss.level != volumeInfo.getCurrentVolume()) {
+                    ss.level = volumeInfo.getCurrentVolume();
                     changed = true;
                 }
                 if (!Objects.equals(ss.remoteLabel, name)) {
@@ -1437,11 +1438,11 @@
         }
 
         @Override
-        public void onRemoteVolumeChanged(Token token, int flags) {
-                addStream(token, "onRemoteVolumeChanged");
+        public void onRemoteVolumeChanged(SessionId sessionId, int flags) {
+                addStream(sessionId, "onRemoteVolumeChanged");
                 int stream = 0;
                 synchronized (mRemoteStreams) {
-                    stream = mRemoteStreams.get(token);
+                    stream = mRemoteStreams.get(sessionId);
                 }
                 final boolean showUI = shouldShowUI(flags);
                 Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI);
@@ -1459,7 +1460,7 @@
         }
 
         @Override
-        public void onRemoteRemoved(Token token) {
+        public void onRemoteRemoved(SessionId token) {
             int stream;
             synchronized (mRemoteStreams) {
                 if (!mRemoteStreams.containsKey(token)) {
@@ -1480,7 +1481,7 @@
         }
 
         public void setStreamVolume(int stream, int level) {
-            final Token token = findToken(stream);
+            final SessionId token = findToken(stream);
             if (token == null) {
                 Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
                 return;
@@ -1488,9 +1489,9 @@
             mMediaSessions.setVolume(token, level);
         }
 
-        private Token findToken(int stream) {
+        private SessionId findToken(int stream) {
             synchronized (mRemoteStreams) {
-                for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) {
+                for (Map.Entry<SessionId, Integer> entry : mRemoteStreams.entrySet()) {
                     if (entry.getValue().equals(stream)) {
                         return entry.getKey();
                     }
@@ -1499,7 +1500,7 @@
             return null;
         }
 
-        private void addStream(Token token, String triggeringMethod) {
+        private void addStream(SessionId token, String triggeringMethod) {
             synchronized (mRemoteStreams) {
                 if (!mRemoteStreams.containsKey(token)) {
                     mRemoteStreams.put(token, mNextStream);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 83b7c18..86defff 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -68,7 +68,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.volume_dialog)
-        requireViewById<View>(R.id.volume_dialog_root).repeatWhenAttached {
+        requireViewById<View>(R.id.volume_dialog).repeatWhenAttached {
             coroutineScopeTraced("[Volume]dialog") {
                 val component = componentFactory.create(this)
                 with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
index 20a74b0..afe3d7b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.volume.dialog.domain.interactor
 
 import android.annotation.SuppressLint
+import android.provider.Settings
 import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
 import com.android.systemui.volume.Events
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
@@ -28,8 +30,9 @@
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible
 import com.android.systemui.volume.dialog.utils.VolumeTracer
 import javax.inject.Inject
-import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -43,8 +46,6 @@
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
-private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds
-
 /**
  * Handles Volume Dialog visibility state. It might change from several sources:
  * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
@@ -60,8 +61,11 @@
     private val tracer: VolumeTracer,
     private val repository: VolumeDialogVisibilityRepository,
     private val controller: VolumeDialogController,
+    private val secureSettingsRepository: SecureSettingsRepository,
 ) {
 
+    private val defaultTimeout = 3.seconds
+
     @SuppressLint("SharedFlowCreation")
     private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
     val dialogVisibility: Flow<VolumeDialogVisibilityModel> =
@@ -73,7 +77,14 @@
     init {
         merge(
                 mutableDismissDialogEvents.mapLatest {
-                    delay(MAX_DIALOG_SHOW_TIME)
+                    delay(
+                        secureSettingsRepository
+                            .getInt(
+                                Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT,
+                                defaultTimeout.toInt(DurationUnit.MILLISECONDS),
+                            )
+                            .milliseconds
+                    )
                     VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
                 },
                 callbacksInteractor.event,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index 3d0c7d6..eb2b2f6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -120,7 +120,7 @@
         drawerContainer.setTransitionListener(ringerDrawerTransitionListener)
         volumeDialogBackgroundView.background = volumeDialogBackgroundView.background.mutate()
         ringerBackgroundView.background = ringerBackgroundView.background.mutate()
-        launch { dialogViewModel.addTouchableBounds(drawerContainer) }
+        launch { dialogViewModel.addTouchableBounds(ringerBackgroundView) }
 
         viewModel.ringerViewModel
             .mapLatest { ringerState ->
@@ -246,16 +246,12 @@
                 uiModel.drawerState.currentMode != uiModel.drawerState.previousMode
         ) {
             val count = uiModel.availableButtons.size
-            val selectedButton =
-                getChildAt(count - uiModel.currentButtonIndex)
-                    .requireViewById<ImageButton>(R.id.volume_drawer_button)
+            val selectedButton = getChildAt(count - uiModel.currentButtonIndex) as ImageButton
             val previousIndex =
                 uiModel.availableButtons.indexOfFirst {
                     it.ringerMode == uiModel.drawerState.previousMode
                 }
-            val unselectedButton =
-                getChildAt(count - previousIndex)
-                    .requireViewById<ImageButton>(R.id.volume_drawer_button)
+            val unselectedButton = getChildAt(count - previousIndex) as ImageButton
             // We only need to execute on roundness animation end and volume dialog background
             // progress update once because these changes should be applied once on volume dialog
             // background and ringer drawer views.
@@ -306,7 +302,7 @@
     ) {
         val count = uiModel.availableButtons.size
         uiModel.availableButtons.fastForEachIndexed { index, ringerButton ->
-            val view = getChildAt(count - index)
+            val view = getChildAt(count - index) as ImageButton
             val isOpen = uiModel.drawerState is RingerDrawerState.Open
             if (index == uiModel.currentButtonIndex) {
                 view.bindDrawerButton(
@@ -323,37 +319,37 @@
         onAnimationEnd?.run()
     }
 
-    private fun View.bindDrawerButton(
+    private fun ImageButton.bindDrawerButton(
         buttonViewModel: RingerButtonViewModel,
         viewModel: VolumeDialogRingerDrawerViewModel,
         isOpen: Boolean,
         isSelected: Boolean = false,
         isAnimated: Boolean = false,
     ) {
+        // id = buttonViewModel.viewId
+        setSelected(isSelected)
         val ringerContentDesc = context.getString(buttonViewModel.contentDescriptionResId)
-        with(requireViewById<ImageButton>(R.id.volume_drawer_button)) {
-            setImageResource(buttonViewModel.imageResId)
-            contentDescription =
-                if (isSelected && !isOpen) {
-                    context.getString(
-                        R.string.volume_ringer_drawer_closed_content_description,
-                        ringerContentDesc,
-                    )
-                } else {
-                    ringerContentDesc
-                }
-            if (isSelected && !isAnimated) {
-                setBackgroundResource(R.drawable.volume_drawer_selection_bg)
-                setColorFilter(context.getColor(internalR.color.materialColorOnPrimary))
-                background = background.mutate()
-            } else if (!isAnimated) {
-                setBackgroundResource(R.drawable.volume_ringer_item_bg)
-                setColorFilter(context.getColor(internalR.color.materialColorOnSurface))
-                background = background.mutate()
+        setImageResource(buttonViewModel.imageResId)
+        contentDescription =
+            if (isSelected && !isOpen) {
+                context.getString(
+                    R.string.volume_ringer_drawer_closed_content_description,
+                    ringerContentDesc,
+                )
+            } else {
+                ringerContentDesc
             }
-            setOnClickListener {
-                viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
-            }
+        if (isSelected && !isAnimated) {
+            setBackgroundResource(R.drawable.volume_drawer_selection_bg)
+            setColorFilter(context.getColor(internalR.color.materialColorOnPrimary))
+            background = background.mutate()
+        } else if (!isAnimated) {
+            setBackgroundResource(R.drawable.volume_ringer_item_bg)
+            setColorFilter(context.getColor(internalR.color.materialColorOnSurface))
+            background = background.mutate()
+        }
+        setOnClickListener {
+            viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index a51e33a..2c9ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -19,7 +19,6 @@
 import android.graphics.drawable.Drawable
 import android.view.View
 import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
@@ -61,11 +60,9 @@
 import javax.inject.Inject
 import kotlin.math.round
 import kotlin.math.roundToInt
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
 
 @VolumeDialogSliderScope
 class VolumeDialogSliderViewBinder
@@ -116,10 +113,6 @@
 
     val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()
 
-    var animateJob: Job? = null
-    val animatedSliderValue =
-        remember(sliderStateModel.value) { Animatable(sliderStateModel.value) }
-
     val interactionSource = remember { MutableInteractionSource() }
     val hapticsViewModel: SliderHapticsViewModel? =
         hapticsViewModelFactory?.let {
@@ -149,16 +142,7 @@
                         hapticsViewModel?.onValueChangeEnded()
                     }
                     sliderState.onValueChange = { newValue ->
-                        if (newValue != animatedSliderValue.targetValue) {
-                            animateJob?.cancel()
-                            animateJob =
-                                coroutineScope.launch {
-                                    animatedSliderValue.animateTo(newValue) {
-                                        sliderState.value = value
-                                    }
-                                }
-                        }
-
+                        sliderState.value = newValue
                         hapticsViewModel?.addVelocityDataPoint(newValue)
                         overscrollViewModel.setSlider(
                             value = sliderState.value,
@@ -173,7 +157,7 @@
     var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
     LaunchedEffect(sliderStateModel.value) {
         val value = sliderStateModel.value
-        launch { animatedSliderValue.animateTo(value) }
+        sliderState.value = value
         if (value != lastDiscreteStep) {
             lastDiscreteStep = value
             hapticsViewModel?.onValueChange(value)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index f2d7d95..7cc4bcc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -74,7 +74,7 @@
         val insets: MutableStateFlow<WindowInsets> =
             MutableStateFlow(WindowInsets.Builder().build())
         // Root view of the Volume Dialog.
-        val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root)
+        val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog)
 
         animateVisibility(root, dialog, viewModel.dialogVisibilityModel)
 
diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt
new file mode 100644
index 0000000..95b3b68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.window.dagger
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/**
+ * Module that can be installed in sysui variants where we support cross window blur.
+ */
+@Module
+interface WindowRootViewBlurModule {
+    @Binds
+    @SysUISingleton
+    fun bindWindowRootViewBlurRepository(
+        windowRootViewBlurRepositoryImpl: WindowRootViewBlurRepositoryImpl
+    ): WindowRootViewBlurRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt
new file mode 100644
index 0000000..ae917e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.window.dagger
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.window.data.repository.NoopWindowRootViewBlurRepository
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
+import dagger.Binds
+import dagger.Module
+
+/**
+ * Module that can be installed in sysui variants where we don't support cross window blur.
+ */
+@Module
+interface WindowRootViewBlurNotSupportedModule {
+    @Binds
+    @SysUISingleton
+    fun bindWindowRootViewBlurRepository(
+        windowRootViewBlurRepositoryImpl: NoopWindowRootViewBlurRepository
+    ): WindowRootViewBlurRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt
new file mode 100644
index 0000000..80aa11a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.window.data.repository
+
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlurRepository {
+    override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0)
+    override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
index 6b7de98..41ceda0 100644
--- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
@@ -16,18 +16,77 @@
 
 package com.android.systemui.window.data.repository
 
-import android.annotation.SuppressLint
+import android.app.ActivityManager
+import android.os.SystemProperties
+import android.view.CrossWindowBlurListeners
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import com.android.systemui.window.data.repository.WindowRootViewBlurRepository.Companion.isDisableBlurSysPropSet
+import java.util.concurrent.Executor
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
 
 /** Repository that maintains state for the window blur effect. */
+interface WindowRootViewBlurRepository {
+    val blurRadius: MutableStateFlow<Int>
+    val isBlurOpaque: MutableStateFlow<Boolean>
+
+    /** Is blur supported based on settings toggle and battery power saver mode. */
+    val isBlurSupported: StateFlow<Boolean>
+
+    companion object {
+        /**
+         * Whether the `persist.sysui.disableBlur` is set, this is used to disable blur for tests.
+         */
+        @JvmStatic
+        fun isDisableBlurSysPropSet() = SystemProperties.getBoolean(DISABLE_BLUR_PROPERTY, false)
+
+        // property that can be used to disable the cross window blur for tests
+        private const val DISABLE_BLUR_PROPERTY = "persist.sysui.disableBlur"
+    }
+}
+
 @SysUISingleton
-class WindowRootViewBlurRepository @Inject constructor() {
-    val blurRadius = MutableStateFlow(0)
+class WindowRootViewBlurRepositoryImpl
+@Inject
+constructor(
+    crossWindowBlurListeners: CrossWindowBlurListeners,
+    @Main private val executor: Executor,
+    @Application private val scope: CoroutineScope,
+) : WindowRootViewBlurRepository {
+    override val blurRadius = MutableStateFlow(0)
 
-    val isBlurOpaque = MutableStateFlow(false)
+    override val isBlurOpaque = MutableStateFlow(false)
 
-    @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>()
+    override val isBlurSupported: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val sendUpdate = { value: Boolean ->
+                    trySendWithFailureLogging(
+                        isBlurAllowed() && value,
+                        TAG,
+                        "unable to send blur enabled/disable state change",
+                    )
+                }
+                crossWindowBlurListeners.addListener(executor, sendUpdate)
+                sendUpdate(crossWindowBlurListeners.isCrossWindowBlurEnabled)
+
+                awaitClose { crossWindowBlurListeners.removeListener(sendUpdate) }
+            } // stateIn because this is backed by a binder call.
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    private fun isBlurAllowed(): Boolean {
+        return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet()
+    }
+
+    companion object {
+        const val TAG = "WindowRootViewBlurRepository"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
index e21e0a1..7a88a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.window.domain.interactor
 
+import android.annotation.SuppressLint
 import android.util.Log
 import com.android.systemui.Flags
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -28,6 +29,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -52,6 +54,8 @@
     private val communalInteractor: CommunalInteractor,
     private val repository: WindowRootViewBlurRepository,
 ) {
+    @SuppressLint("SharedFlowCreation") private val _onBlurAppliedEvent = MutableSharedFlow<Int>()
+
     private var isBouncerTransitionInProgress: StateFlow<Boolean> =
         if (Flags.bouncerUiRevamp()) {
             keyguardTransitionInteractor
@@ -68,16 +72,22 @@
      * root view.
      */
     suspend fun onBlurApplied(appliedBlurRadius: Int) {
-        repository.onBlurApplied.emit(appliedBlurRadius)
+        _onBlurAppliedEvent.emit(appliedBlurRadius)
     }
 
+    /**
+     * Whether blur is enabled or not based on settings toggle, critical thermal state, battery save
+     * state and multimedia tunneling state.
+     */
+    val isBlurCurrentlySupported: StateFlow<Boolean> = repository.isBlurSupported
+
     /** Radius of blur to be applied on the window root view. */
     val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow()
 
     /**
      * Emits the applied blur radius whenever blur is successfully applied to the window root view.
      */
-    val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied
+    val onBlurAppliedEvent: Flow<Int> = _onBlurAppliedEvent
 
     /** Whether the blur applied is opaque or transparent. */
     val isBlurOpaque: Flow<Boolean> =
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
new file mode 100644
index 0000000..eb3ba82
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ViewFlipper
+        android:id="@+id/flipper"
+        android:layout_width="match_parent"
+        android:layout_height="400dp"
+        android:flipInterval="1000"
+        />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
new file mode 100644
index 0000000..e2a00bd
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/imageview"
+    android:layout_width="match_parent"
+    android:layout_height="400dp" />
\ No newline at end of file
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 e8054c0..4ccfa29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -206,7 +206,6 @@
     private @Mock ShadeInteractor mShadeInteractor;
     private @Mock ShadeWindowLogger mShadeWindowLogger;
     private @Mock SelectedUserInteractor mSelectedUserInteractor;
-    private @Mock UserTracker.Callback mUserTrackerCallback;
     private @Mock KeyguardInteractor mKeyguardInteractor;
     private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
     private @Captor ArgumentCaptor<KeyguardStateController.Callback>
@@ -281,7 +280,7 @@
                 () -> mShadeInteractor,
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
-                mock(UserTracker.class),
+                mUserTracker,
                 mKosmos.getNotificationShadeWindowModel(),
                 mSecureSettings,
                 mKosmos::getCommunalInteractor,
@@ -319,7 +318,7 @@
 
         } catch (Exception e) {
             // Just so we don't have to add the exception signature to every test.
-            fail(e.getMessage());
+            fail();
         }
     }
 
@@ -331,156 +330,18 @@
 
         /* First test the default behavior: handleUserSwitching() is not invoked */
         when(mUserTracker.isUserSwitching()).thenReturn(false);
+        mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class);
         mViewMediator.onSystemReady();
         TestableLooper.get(this).processAllMessages();
 
-        verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext),
-                any(Runnable.class));
+        verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId);
 
         /* Next test user switching is already in progress when started */
         when(mUserTracker.isUserSwitching()).thenReturn(true);
         mViewMediator.onSystemReady();
         TestableLooper.get(this).processAllMessages();
 
-        verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext),
-                any(Runnable.class));
-    }
-
-    @Test
-    @TestableLooper.RunWithLooper(setAsMainLooper = true)
-    public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() {
-        setCurrentUser(/* userId= */1099, /* isSecure= */false);
-
-        // Setup keyguard
-        mViewMediator.onSystemReady();
-        processAllMessagesAndBgExecutorMessages();
-        mViewMediator.setShowingLocked(true, "");
-
-        // Request keyguard going away
-        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
-        mViewMediator.mKeyguardGoingAwayRunnable.run();
-
-        // After the request, begin a switch to a new secure user
-        int nextUserId = 500;
-        setCurrentUser(nextUserId, /* isSecure= */true);
-        Runnable result = mock(Runnable.class);
-        mViewMediator.handleBeforeUserSwitching(nextUserId, result);
-        processAllMessagesAndBgExecutorMessages();
-        verify(result).run();
-
-        // After that request has begun, have WM tell us to exit keyguard
-        RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{
-                mock(RemoteAnimationTarget.class)
-        };
-        RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{
-                mock(RemoteAnimationTarget.class)
-        };
-        IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
-        mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
-                null, callback);
-        processAllMessagesAndBgExecutorMessages();
-
-        // The call to exit should be rejected, and keyguard should still be visible
-        verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation(
-                any(), any(), any(), anyLong(), anyBoolean());
-        try {
-            assertATMSLockScreenShowing(true);
-        } catch (Exception e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mViewMediator.isShowingAndNotOccluded());
-    }
-
-    @Test
-    @TestableLooper.RunWithLooper(setAsMainLooper = true)
-    public void testUserSwitchToSecureUserShowsBouncer() {
-        setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
-        // Setup keyguard
-        mViewMediator.onSystemReady();
-        processAllMessagesAndBgExecutorMessages();
-        mViewMediator.setShowingLocked(true, "");
-
-        // After the request, begin a switch to a new secure user
-        int nextUserId = 500;
-        setCurrentUser(nextUserId, /* isSecure= */true);
-
-        Runnable beforeResult = mock(Runnable.class);
-        mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
-        processAllMessagesAndBgExecutorMessages();
-        verify(beforeResult).run();
-
-        // Dismiss should not be called while user switch is in progress
-        Runnable onSwitchResult = mock(Runnable.class);
-        mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
-        processAllMessagesAndBgExecutorMessages();
-        verify(onSwitchResult).run();
-        verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse();
-
-        // The attempt to dismiss only comes on user switch complete, which will trigger a call to
-        // show the bouncer in StatusBarKeyguardViewManager
-        mViewMediator.handleUserSwitchComplete(nextUserId);
-        TestableLooper.get(this).moveTimeForward(600);
-        processAllMessagesAndBgExecutorMessages();
-
-        verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
-    }
-
-    @Test
-    @TestableLooper.RunWithLooper(setAsMainLooper = true)
-    public void testUserSwitchToInsecureUserDismissesKeyguard() {
-        int userId = 1099;
-        when(mUserTracker.getUserId()).thenReturn(userId);
-
-        // Setup keyguard
-        mViewMediator.onSystemReady();
-        processAllMessagesAndBgExecutorMessages();
-        mViewMediator.setShowingLocked(true, "");
-
-        // After the request, begin a switch to an insecure user
-        int nextUserId = 500;
-        when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false);
-
-        Runnable beforeResult = mock(Runnable.class);
-        mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
-        processAllMessagesAndBgExecutorMessages();
-        verify(beforeResult).run();
-
-        // The call to dismiss comes during the user switch
-        Runnable onSwitchResult = mock(Runnable.class);
-        mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
-        processAllMessagesAndBgExecutorMessages();
-        verify(onSwitchResult).run();
-
-        verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
-    }
-
-    @Test
-    @TestableLooper.RunWithLooper(setAsMainLooper = true)
-    public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() {
-        setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
-        // Setup keyguard as not visible
-        mViewMediator.onSystemReady();
-        processAllMessagesAndBgExecutorMessages();
-        mViewMediator.setShowingLocked(false, "");
-        processAllMessagesAndBgExecutorMessages();
-
-        // Begin a switch to a new secure user
-        int nextUserId = 500;
-        setCurrentUser(nextUserId, /* isSecure= */true);
-
-        Runnable beforeResult = mock(Runnable.class);
-        mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
-        processAllMessagesAndBgExecutorMessages();
-        verify(beforeResult).run();
-
-        try {
-            assertATMSLockScreenShowing(true);
-        } catch (Exception e) {
-            fail();
-        }
-        assertTrue(mViewMediator.isShowingAndNotOccluded());
+        verify(mViewMediator.mUpdateCallback).onUserSwitching(userId);
     }
 
     @Test
@@ -1244,7 +1105,7 @@
         processAllMessagesAndBgExecutorMessages();
 
         verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
-
+        assertATMSAndKeyguardViewMediatorStatesMatch();
     }
 
     @Test
@@ -1288,7 +1149,6 @@
         IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
 
         when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
-        mViewMediator.mKeyguardGoingAwayRunnable.run();
         mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
                 null, callback);
         processAllMessagesAndBgExecutorMessages();
@@ -1343,6 +1203,13 @@
 
         // The captor will have the most recent setLockScreenShown call's value.
         assertEquals(showing, showingCaptor.getValue());
+
+        // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be
+        // showing, ensure that we didn't subsequently ask for it to go away.
+        if (showing) {
+            orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never())
+                    .keyguardGoingAway(anyInt());
+        }
     }
 
     /**
@@ -1504,7 +1371,6 @@
                 mKeyguardTransitionBootInteractor,
                 mKosmos::getCommunalSceneInteractor,
                 mock(WindowManagerOcclusionManager.class));
-        mViewMediator.mUserChangedCallback = mUserTrackerCallback;
         mViewMediator.start();
 
         mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null);
@@ -1518,10 +1384,4 @@
     private void captureKeyguardUpdateMonitorCallback() {
         verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture());
     }
-
-    private void setCurrentUser(int userId, boolean isSecure) {
-        when(mUserTracker.getUserId()).thenReturn(userId);
-        when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId);
-        when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure);
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 732561e0..944604f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -640,11 +640,11 @@
             }
         }
 
-    @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
     @Test
     fun onTouchEvent_shadeInteracting_movesNotDispatched() =
         with(kosmos) {
             testScope.runTest {
+                `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true)
                 // On lockscreen.
                 goToScene(CommunalScenes.Blank)
                 whenever(
@@ -721,11 +721,11 @@
             }
         }
 
-    @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
     @Test
     fun onTouchEvent_bouncerInteracting_movesNotDispatched() =
         with(kosmos) {
             testScope.runTest {
+                `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true)
                 // On lockscreen.
                 goToScene(CommunalScenes.Blank)
                 whenever(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index edb0f35..f3af794f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -59,6 +59,7 @@
 import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.NextAlarmController
@@ -202,6 +203,7 @@
                 Lazy { kosmos.shadeDisplaysRepository },
                 variableDateViewControllerFactory,
                 batteryMeterViewController,
+                kosmos.batteryViewModelFactory,
                 dumpManager,
                 mShadeCarrierGroupControllerBuilder,
                 combinedShadeHeadersConstraintManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
new file mode 100644
index 0000000..09fa387
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.DisableFlags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase {
+
+    @Rule
+    public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+    @Test
+    @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS
+    })
+    public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() {
+        NotificationEntry entry = buildAcceptableNotificationEntry(mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isFalse();
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
new file mode 100644
index 0000000..1cadb3c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase {
+
+    private static final String AUTHORITY = "notification.memory.test.authority";
+    private static final Uri TEST_URI = new Uri.Builder()
+            .scheme("content")
+            .authority(AUTHORITY)
+            .path("path")
+            .build();
+
+    @Rule
+    public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+    @Before
+    public void setUp() {
+        TestImageContentProvider provider = new TestImageContentProvider(mContext);
+        mContext.getContentResolver().addProvider(AUTHORITY, provider);
+        provider.onCreate();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() {
+        NotificationEntry entry =
+                buildAcceptableNotificationEntry(
+                        mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() {
+        Notification notification =
+                new Notification.Builder(mContext, "ChannelId")
+                        .setContentTitle("Just a notification")
+                        .setContentText("Yep")
+                        .build();
+        NotificationEntry entry = new NotificationEntryBuilder().setNotification(
+                notification).build();
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isFalse();
+    }
+
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_smallNotification_returnsTrue() {
+        Notification.Builder notification =
+                buildAcceptableNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        )
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() {
+        Notification.Builder notification =
+                buildOversizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        ).isFalse();
+    }
+
+    @Test
+    @DisableCompatChanges(
+            {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}
+    )
+    public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() {
+        Notification.Builder notification =
+                buildOversizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        ).isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() {
+        Notification.Builder notification =
+                buildWarningSizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        )
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() {
+        NotificationEntry entry = new NotificationEntryBuilder().build();
+        View view = new FrameLayout(mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry))
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() {
+        Notification.Builder notification =
+                buildAcceptableNotification(mContext,
+                        TEST_URI);
+        // This should have a size of a single image
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize(
+                        inflatedView))
+                .isGreaterThan(170000);
+    }
+
+    private View inflateNotification(Notification.Builder builder) {
+        RemoteViews remoteViews = builder.createBigContentView();
+        return remoteViews.apply(mContext, new FrameLayout(mContext));
+    }
+
+    private NotificationEntry toEntry(Notification.Builder builder) {
+        return new NotificationEntryBuilder().setNotification(builder.build())
+                .setUid(Process.myUid()).build();
+    }
+
+
+    /** This provider serves the images for inflation. */
+    class TestImageContentProvider extends ContentProvider {
+
+        TestImageContentProvider(Context context) {
+            ProviderInfo info = new ProviderInfo();
+            info.authority = AUTHORITY;
+            info.exported = true;
+            attachInfoForTesting(context, info);
+            setAuthorities(AUTHORITY);
+        }
+
+        @Override
+        public boolean onCreate() {
+            return true;
+        }
+
+        @Override
+        public ParcelFileDescriptor openFile(Uri uri, String mode) {
+            return getContext().getResources().openRawResourceFd(
+                    NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE())
+                        .getParcelFileDescriptor();
+        }
+
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) {
+            return getContext().getResources().openRawResourceFd(
+                    NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE());
+        }
+
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
+                CancellationSignal signal) throws FileNotFoundException {
+            return openTypedAssetFile(uri, mimeTypeFilter, opts);
+        }
+
+        @Override
+        public int delete(Uri uri, Bundle extras) {
+            return 0;
+        }
+
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            return 0;
+        }
+
+        @Override
+        public String getType(Uri uri) {
+            return "image/png";
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            return null;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values, Bundle extras) {
+            return super.insert(uri, values, extras);
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, Bundle queryArgs,
+                CancellationSignal cancellationSignal) {
+            return super.query(uri, projection, queryArgs, cancellationSignal);
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return null;
+        }
+
+        @Override
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            return 0;
+        }
+    }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
new file mode 100644
index 0000000..ca4f24d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.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
+ */
+
+@file:JvmName("NotificationCustomContentNotificationBuilder")
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.DecoratedCustomViewStyle
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Process
+import android.widget.RemoteViews
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.tests.R
+import org.hamcrest.Matchers.lessThan
+import org.junit.Assume.assumeThat
+
+public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway
+
+fun buildAcceptableNotificationEntry(context: Context): NotificationEntry {
+    return NotificationEntryBuilder()
+        .setNotification(buildAcceptableNotification(context, null).build())
+        .setUid(Process.myUid())
+        .build()
+}
+
+fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder =
+    buildNotification(context, uri, 1)
+
+fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder {
+    val numImagesForOversize =
+        (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) /
+            drawableSizeOnDevice(context)) + 2
+    return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder {
+    val numImagesForOversize =
+        (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) /
+            drawableSizeOnDevice(context)) + 1
+    // The size needs to be smaller than outright stripping size.
+    assumeThat(
+        numImagesForOversize * drawableSizeOnDevice(context),
+        lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)),
+    )
+    return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder {
+    val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper)
+    repeat(numImages) { i ->
+        val remoteViewFlipperImageView =
+            RemoteViews(context.packageName, R.layout.custom_view_flipper_image)
+
+        if (uri == null) {
+            remoteViewFlipperImageView.setImageViewResource(
+                R.id.imageview,
+                R.drawable.romainguy_rockaway,
+            )
+        } else {
+            val imageUri = uri.buildUpon().appendPath(i.toString()).build()
+            remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri)
+        }
+        remoteViews.addView(R.id.flipper, remoteViewFlipperImageView)
+    }
+
+    return Notification.Builder(context, "ChannelId")
+        .setSmallIcon(android.R.drawable.ic_info)
+        .setStyle(DecoratedCustomViewStyle())
+        .setCustomContentView(remoteViews)
+        .setCustomBigContentView(remoteViews)
+        .setContentTitle("This is a remote view!")
+}
+
+fun drawableSizeOnDevice(context: Context): Int {
+    val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE)
+    return (drawable as BitmapDrawable).bitmap.allocationByteCount
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index a523488..9137215 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -63,6 +63,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
@@ -118,10 +119,8 @@
     @Rule public Expect mExpect = Expect.create();
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
 
-    private final FakeConfigurationController mConfigurationController =
-            new FakeConfigurationController();
-    private final LargeScreenShadeInterpolator
-            mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+    private FakeConfigurationController mConfigurationController;
+    private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator;
 
     private final TestScope mTestScope = mKosmos.getTestScope();
     private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
@@ -137,6 +136,7 @@
     private boolean mAlwaysOnEnabled;
     private TestableLooper mLooper;
     private Context mContext;
+
     @Mock private DozeParameters mDozeParameters;
     @Mock private LightBarController mLightBarController;
     @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory;
@@ -149,12 +149,11 @@
     @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
     @Mock private AlternateBouncerToGoneTransitionViewModel
             mAlternateBouncerToGoneTransitionViewModel;
-    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
-            mKosmos.getKeyguardTransitionInteractor();
-    private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
-            mKosmos.getKeyguardTransitionRepository();
     @Mock private KeyguardInteractor mKeyguardInteractor;
 
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private FakeKeyguardTransitionRepository mKeyguardTransitionRepository;
+
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
     //   event-dispatch-on-registration pattern caused some of these unit tests to fail.)
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -238,6 +237,9 @@
         when(mContext.getColor(com.android.internal.R.color.materialColorSurface))
                 .thenAnswer(invocation -> mSurfaceColor);
 
+        mConfigurationController = new FakeConfigurationController();
+        mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+
         mScrimBehind = spy(new ScrimView(mContext));
         mScrimInFront = new ScrimView(mContext);
         mNotificationsScrim = new ScrimView(mContext);
@@ -270,6 +272,9 @@
         when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
                 .thenReturn(emptyFlow());
 
+        mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository();
+        mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor();
+
         mScrimController = new ScrimController(
                 mLightBarController,
                 mDozeParameters,
@@ -290,7 +295,8 @@
                 mKeyguardInteractor,
                 mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator,
-                new BlurConfig(0.0f, 0.0f));
+                new BlurConfig(0.0f, 0.0f),
+                mKosmos::getWindowRootViewBlurInteractor);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -321,6 +327,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguard() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
@@ -336,7 +343,7 @@
     }
 
     @Test
-    public void transitionToShadeLocked() {
+@DisableSceneContainer void transitionToShadeLocked() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setQsPosition(1f, 0);
         finishAnimationsImmediately();
@@ -372,6 +379,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToShadeLocked_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -390,6 +398,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToOff() {
         mScrimController.legacyTransitionTo(ScrimState.OFF);
         finishAnimationsImmediately();
@@ -405,6 +414,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_withRegularWallpaper() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         finishAnimationsImmediately();
@@ -420,6 +430,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_withFrontAlphaUpdates() {
         // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -464,6 +475,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() {
         // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -505,6 +517,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToPulsing_withFrontAlphaUpdates() {
         // Pre-condition
         // Need to go to AoD first because PULSING doesn't change
@@ -550,6 +563,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguardBouncer() {
         mScrimController.legacyTransitionTo(BOUNCER);
         finishAnimationsImmediately();
@@ -570,6 +584,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void lockscreenToHubTransition_setsBehindScrimAlpha() {
         // Start on lockscreen.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -616,6 +631,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void hubToLockscreenTransition_setsViewAlpha() {
         // Start on glanceable hub.
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
@@ -662,6 +678,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToHub() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -676,6 +693,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openBouncerOnHub() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
 
@@ -705,6 +723,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openShadeOnHub() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
 
@@ -733,6 +752,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToHubOverDream() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -747,6 +767,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openBouncerOnHubOverDream() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
 
@@ -776,6 +797,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openShadeOnHubOverDream() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
 
@@ -804,6 +826,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
         assertEquals(BOUNCER.getBehindTint(), 0x112233);
         mSurfaceColor = 0x223344;
@@ -812,6 +835,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -824,6 +848,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguardBouncer_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -844,6 +869,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -866,6 +892,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -888,6 +915,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToBouncer() {
         mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED);
         finishAnimationsImmediately();
@@ -901,6 +929,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlocked_clippedQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.setRawPanelExpansionFraction(0f);
@@ -959,6 +988,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.setRawPanelExpansionFraction(0f);
@@ -998,6 +1028,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimStateCallback() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1013,6 +1044,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void panelExpansion() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1035,6 +1067,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion() {
         reset(mScrimBehind);
         mScrimController.setQsPosition(1f, 999 /* value doesn't matter */);
@@ -1047,6 +1080,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_clippingQs() {
         reset(mScrimBehind);
         mScrimController.setClipsQsScrim(true);
@@ -1060,6 +1094,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_half_clippingQs() {
         reset(mScrimBehind);
         mScrimController.setClipsQsScrim(true);
@@ -1073,6 +1108,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void panelExpansionAffectsAlpha() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1095,6 +1131,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlockedFromOff() {
         // Simulate unlock with fingerprint without AOD
         mScrimController.legacyTransitionTo(ScrimState.OFF);
@@ -1117,6 +1154,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlockedFromAod() {
         // Simulate unlock with fingerprint
         mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1139,6 +1177,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimBlanksBeforeLeavingAod() {
         // Simulate unlock with fingerprint
         mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1162,6 +1201,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimBlankCallbackWhenUnlockingFromPulse() {
         boolean[] blanked = {false};
         // Simulate unlock with fingerprint
@@ -1180,6 +1220,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void blankingNotRequired_leavingAoD() {
         // GIVEN display does NOT need blanking
         when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
@@ -1204,7 +1245,8 @@
                 mKeyguardInteractor,
                 mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator,
-                new BlurConfig(0.0f, 0.0f));
+                new BlurConfig(0.0f, 0.0f),
+                mKosmos::getWindowRootViewBlurInteractor);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -1234,6 +1276,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallback() {
         int[] callOrder = {0, 0, 0};
         int[] currentCall = {0};
@@ -1260,12 +1303,14 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallbacksWithoutAmbientDisplay() {
         mAlwaysOnEnabled = false;
         testScrimCallback();
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallbackCancelled() {
         boolean[] cancelledCalled = {false};
         mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() {
@@ -1279,6 +1324,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testHoldsWakeLock_whenAOD() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         verify(mWakeLock).acquire(anyString());
@@ -1288,6 +1334,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoesNotHoldWakeLock_whenUnlocking() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1295,6 +1342,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testCallbackInvokedOnSameStateTransition() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1304,6 +1352,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testConservesExpansionOpacityAfterTransition() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1321,6 +1370,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testCancelsOldAnimationBeforeBlanking() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         finishAnimationsImmediately();
@@ -1333,6 +1383,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsAreNotFocusable() {
         assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable());
         assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable());
@@ -1341,6 +1392,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testEatsTouchEvent() {
         HashSet<ScrimState> eatsTouches =
                 new HashSet<>(Collections.singletonList(ScrimState.AOD));
@@ -1357,6 +1409,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testAnimatesTransitionToAod() {
         when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
         ScrimState.AOD.prepare(ScrimState.KEYGUARD);
@@ -1371,6 +1424,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testIsLowPowerMode() {
         HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList(
                 ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING));
@@ -1388,6 +1442,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsOpaque_whenShadeFullyExpanded() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setRawPanelExpansionFraction(1);
@@ -1402,6 +1457,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisible() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1417,6 +1473,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoesntAnimate_whenUnlocking() {
         // LightRevealScrim will animate the transition, we should only hide the keyguard scrims.
         ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD);
@@ -1437,6 +1494,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisible_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1452,6 +1510,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisibleOnLockscreen() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         mScrimController.setQsPosition(0.25f, 300);
@@ -1463,6 +1522,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationScrimTransparent_whenOnLockscreen() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         // even if shade is not pulled down, panel has expansion of 1 on the lockscreen
@@ -1475,6 +1535,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() {
         mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -1486,6 +1547,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         // clipping doesn't change tested logic but allows to assert scrims more in line with
@@ -1502,6 +1564,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
 
@@ -1518,6 +1581,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
@@ -1533,6 +1597,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
@@ -1552,6 +1617,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         mScrimController.setClipsQsScrim(true);
@@ -1572,6 +1638,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
         mScrimController.setClipsQsScrim(true);
@@ -1592,6 +1659,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
         mScrimController.setClipsQsScrim(false);
@@ -1603,6 +1671,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.setClipsQsScrim(true);
 
@@ -1644,6 +1713,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationTransparency_followsNotificationScrimProgress() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setRawPanelExpansionFraction(1.0f);
@@ -1660,6 +1730,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1695,6 +1766,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() {
         int overScrollAmount = 10;
 
@@ -1704,6 +1776,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() {
         int overScrollAmount = 10;
 
@@ -1713,6 +1786,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() {
         int overScrollAmount = 10;
 
@@ -1722,6 +1796,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationBoundsTopGetsPassedToKeyguard() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setQsPosition(1f, 0);
@@ -1732,6 +1807,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() {
         mScrimController.setKeyguardOccluded(true);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1742,6 +1818,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToDreaming() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -1761,6 +1838,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void keyguardGoingAwayUpdateScrims() {
         when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
         mScrimController.updateScrims();
@@ -1770,6 +1848,7 @@
 
 
     @Test
+    @DisableSceneContainer
     public void setUnOccludingAnimationKeyguard() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
@@ -1784,6 +1863,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testHidesScrimFlickerInActivity() {
         mScrimController.setKeyguardOccluded(true);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1802,6 +1882,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1811,6 +1892,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void aodStateSetsFrontScrimToNotBlend() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         assertFalse("Front scrim should not blend with main color",
@@ -1818,6 +1900,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void applyState_unlocked_bouncerShowing() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setBouncerHiddenFraction(0.99f);
@@ -1827,6 +1910,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void ignoreTransitionRequestWhileKeyguardTransitionRunning() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1839,6 +1923,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() {
         when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1849,6 +1934,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void primaryBouncerToGoneOnFinishCallsLightBarController() {
         reset(mLightBarController);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1860,6 +1946,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoNotAnimateChangeIfOccludeAnimationPlaying() {
         mScrimController.setOccludeAnimationPlaying(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1868,6 +1955,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 2f30b74..3190d3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -91,11 +91,11 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository;
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -122,7 +122,6 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
 
-    private static final int DISPLAY_ID = 0;
     private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
@@ -233,7 +232,6 @@
         mNotificationActivityStarter =
                 new StatusBarNotificationActivityStarter(
                         getContext(),
-                        DISPLAY_ID,
                         mHandler,
                         mUiBgExecutor,
                         mVisibilityProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index fcdda9f..9da8e80 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import android.service.dream.dreamManager
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
@@ -44,5 +45,6 @@
             deviceEntryInteractor = deviceEntryInteractor,
             wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
             dreamManager = dreamManager,
+            communalSettingsInteractor = communalSettingsInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index b255b51..0443329 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -57,12 +57,10 @@
 
 var Kosmos.testScope by Fixture { TestScope(testDispatcher) }
 var Kosmos.backgroundScope by Fixture { testScope.backgroundScope }
-var Kosmos.applicationCoroutineScope by Fixture { backgroundScope }
+var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope }
 var Kosmos.testCase: SysuiTestCase by Fixture()
-var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture {
-    backgroundScope.coroutineContext
-}
-var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext }
+var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testDispatcher }
+var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testDispatcher }
 
 /**
  * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 446e106..60b371a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -90,6 +90,7 @@
 import com.android.systemui.statusbar.ui.viewmodel.keyguardStatusBarViewModel
 import com.android.systemui.util.time.systemClock
 import com.android.systemui.volume.domain.interactor.volumeDialogInteractor
+import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
 
 /**
  * Helper for using [Kosmos] from Java.
@@ -192,4 +193,5 @@
     val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor }
     val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
     val mockWindowRootViewProvider by lazy { kosmos.mockWindowRootViewProvider }
+    val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
index 5fc31f8..f287114 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -79,9 +80,9 @@
         with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles }
     }
 
-    override val tilesReadFromSetting: Channel<Pair<Set<TileSpec>, Int>> = Channel(capacity = 10)
+    override val tilesUpgradePath: Channel<Pair<TilesUpgradePath, Int>> = Channel(capacity = 10)
 
-    suspend fun sendTilesReadFromSetting(tiles: Set<TileSpec>, userId: Int) {
-        tilesReadFromSetting.send(tiles to userId)
+    suspend fun sendTilesFromUpgradePath(upgradePath: TilesUpgradePath, userId: Int) {
+        tilesUpgradePath.send(upgradePath to userId)
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
index 5ff44e5..c5de02a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
@@ -26,7 +26,7 @@
     Kosmos.Fixture { fakeMinimumTilesRepository }
 
 var Kosmos.fakeDefaultTilesRepository by Kosmos.Fixture { FakeDefaultTilesRepository() }
-val Kosmos.defaultTilesRepository: DefaultTilesRepository by
+var Kosmos.defaultTilesRepository: DefaultTilesRepository by
     Kosmos.Fixture { fakeDefaultTilesRepository }
 
 val Kosmos.fakeTileSpecRepository by
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 ce298bb..f0350ac 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
@@ -5,6 +5,8 @@
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.haptics.msdl.msdlPlayer
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lightRevealScrimViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -18,7 +20,6 @@
 import com.android.systemui.scene.ui.composable.ConstantSceneContainerTransitionsBuilder
 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.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.domain.interactor.shadeModeInteractor
 import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
@@ -97,7 +98,6 @@
                 powerInteractor = powerInteractor,
                 shadeModeInteractor = shadeModeInteractor,
                 remoteInputInteractor = remoteInputInteractor,
-                splitEdgeDetector = splitEdgeDetector,
                 logger = sceneLogger,
                 hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
                 view = view,
@@ -105,6 +105,8 @@
                 lightRevealScrim = lightRevealScrimViewModel,
                 wallpaperViewModel = wallpaperViewModel,
                 keyguardInteractor = keyguardInteractor,
+                burnIn = aodBurnInViewModel,
+                clock = keyguardClockViewModel,
             )
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
deleted file mode 100644
index e0b5292..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.scene.ui.viewmodel
-
-import androidx.compose.ui.unit.dp
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-
-var Kosmos.splitEdgeDetector: SplitEdgeDetector by
-    Kosmos.Fixture {
-        SplitEdgeDetector(
-            topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
-            edgeSize = 40.dp,
-        )
-    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
index b40e1e7..6b64193 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.data.repository
 
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 
 /**
  * Make the repository hold [count] active notifications for testing. The keys of the notifications
@@ -37,3 +38,56 @@
             }
             .build()
 }
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notif] will be ranked highest.
+ */
+fun ActiveNotificationListRepository.addNotif(notif: ActiveNotificationModel) {
+    val currentNotifications = this.activeNotifications.value.individuals
+    this.activeNotifications.value =
+        ActiveNotificationsStore.Builder()
+            .apply {
+                addIndividualNotif(notif)
+                currentNotifications.forEach {
+                    if (it.key != notif.key) {
+                        addIndividualNotif(it.value)
+                    }
+                }
+            }
+            .build()
+}
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notifs] will be ranked higher than existing notifs.
+ */
+fun ActiveNotificationListRepository.addNotifs(notifs: List<ActiveNotificationModel>) {
+    val currentNotifications = this.activeNotifications.value.individuals
+    val newKeys = notifs.map { it.key }
+    this.activeNotifications.value =
+        ActiveNotificationsStore.Builder()
+            .apply {
+                notifs.forEach { addIndividualNotif(it) }
+                currentNotifications.forEach {
+                    if (!newKeys.contains(it.key)) {
+                        addIndividualNotif(it.value)
+                    }
+                }
+            }
+            .build()
+}
+
+fun ActiveNotificationListRepository.removeNotif(keyToRemove: String) {
+    val currentNotifications = this.activeNotifications.value.individuals
+    this.activeNotifications.value =
+        ActiveNotificationsStore.Builder()
+            .apply {
+                currentNotifications.forEach {
+                    if (it.key != keyToRemove) {
+                        addIndividualNotif(it.value)
+                    }
+                }
+            }
+            .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
index 2057b84..c7380c9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
 import com.android.systemui.statusbar.lockscreenShadeTransitionController
 
 val Kosmos.notificationShelfInteractor by Fixture {
@@ -28,6 +29,7 @@
         keyguardRepository = keyguardRepository,
         deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository,
         powerInteractor = powerInteractor,
+        shadeModeInteractor = shadeModeInteractor,
         keyguardTransitionController = lockscreenShadeTransitionController,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
index 0d6ac44..d787e2c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
@@ -52,7 +52,6 @@
     Kosmos.Fixture {
         StatusBarNotificationActivityStarter(
             applicationContext,
-            applicationContext.displayId,
             fakeExecutorHandler,
             fakeExecutor,
             notificationVisibilityProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
deleted file mode 100644
index 923b36d..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.ongoingcall.shared.model
-
-import android.app.PendingIntent
-import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-
-/** Helper for building [OngoingCallModel.InCall] instances in tests. */
-fun inCallModel(
-    startTimeMs: Long,
-    notificationIcon: StatusBarIconView? = null,
-    intent: PendingIntent? = null,
-    notificationKey: String = "test",
-    appName: String = "",
-    promotedContent: PromotedNotificationContentModel? = null,
-) =
-    OngoingCallModel.InCall(
-        startTimeMs,
-        notificationIcon,
-        intent,
-        notificationKey,
-        appName,
-        promotedContent,
-    )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
new file mode 100644
index 0000000..d09d010
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ongoingcall.shared.model
+
+import android.app.PendingIntent
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.removeNotif
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
+import org.mockito.kotlin.mock
+
+/** Helper for building [OngoingCallModel.InCall] instances in tests. */
+fun inCallModel(
+    startTimeMs: Long,
+    notificationIcon: StatusBarIconView? = null,
+    intent: PendingIntent? = null,
+    notificationKey: String = "test",
+    appName: String = "",
+    promotedContent: PromotedNotificationContentModel? = null,
+) =
+    OngoingCallModel.InCall(
+        startTimeMs,
+        notificationIcon,
+        intent,
+        notificationKey,
+        appName,
+        promotedContent,
+    )
+
+object OngoingCallTestHelper {
+    /**
+     * Removes any ongoing call state and removes any call notification associated with [key]. Does
+     * it correctly based on whether [StatusBarChipsModernization] is enabled or not.
+     *
+     * @param key the notification key associated with the call notification.
+     */
+    fun Kosmos.removeOngoingCallState(key: String) {
+        if (StatusBarChipsModernization.isEnabled) {
+            activeNotificationListRepository.removeNotif(key)
+        } else {
+            ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
+        }
+    }
+
+    /**
+     * Sets SysUI to have an ongoing call state. Does it correctly based on whether
+     * [StatusBarChipsModernization] is enabled or not.
+     *
+     * @param key the notification key to be associated with the call notification
+     */
+    fun Kosmos.addOngoingCallState(
+        key: String = "notif",
+        startTimeMs: Long = 1000L,
+        statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
+        promotedContent: PromotedNotificationContentModel? = null,
+        contentIntent: PendingIntent? = null,
+        uid: Int = DEFAULT_UID,
+    ) {
+        if (StatusBarChipsModernization.isEnabled) {
+            activeNotificationListRepository.addNotif(
+                activeNotificationModel(
+                    key = key,
+                    whenTime = startTimeMs,
+                    callType = CallType.Ongoing,
+                    statusBarChipIcon = statusBarChipIconView,
+                    contentIntent = contentIntent,
+                    promotedContent = promotedContent,
+                    uid = uid,
+                )
+            )
+        } else {
+            ongoingCallRepository.setOngoingCallState(
+                inCallModel(
+                    startTimeMs = startTimeMs,
+                    notificationIcon = statusBarChipIconView,
+                    intent = contentIntent,
+                    notificationKey = key,
+                    promotedContent = promotedContent,
+                )
+            )
+        }
+    }
+
+    private fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+        if (StatusBarConnectedDisplays.isEnabled) {
+            null
+        } else {
+            mock<StatusBarIconView>()
+        }
+
+    private const val DEFAULT_UID = 886
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt
index c6cf006..7dd0103 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt
@@ -22,3 +22,10 @@
 
 val Kosmos.batteryViewModel by
     Kosmos.Fixture { BatteryViewModel(batteryInteractor, testableContext) }
+
+val Kosmos.batteryViewModelFactory by
+    Kosmos.Fixture {
+        object : BatteryViewModel.Factory {
+            override fun create(): BatteryViewModel = batteryViewModel
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index bc29dba..fbada93 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor
 import com.android.systemui.statusbar.phone.domain.interactor.lightsOutInteractor
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarIconBlockListInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarInteractor
 
@@ -42,6 +43,7 @@
     Kosmos.Fixture {
         HomeStatusBarViewModelImpl(
             testableContext.displayId,
+            batteryViewModelFactory,
             tableLogBufferFactory,
             homeStatusBarInteractor,
             homeStatusBarIconBlockListInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index 0d2aa4c..888b7e6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
 import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
 import com.android.systemui.volume.dialog.utils.volumeTracer
 
@@ -30,5 +31,6 @@
             volumeTracer,
             volumeDialogVisibilityRepository,
             volumeDialogController,
+            secureSettingsRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
index 7281e03..9699223 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
@@ -17,5 +17,16 @@
 package com.android.systemui.window.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.flow.MutableStateFlow
 
-val Kosmos.windowRootViewBlurRepository by Kosmos.Fixture { WindowRootViewBlurRepository() }
+val Kosmos.fakeWindowRootViewBlurRepository: FakeWindowRootViewBlurRepository by
+    Kosmos.Fixture { FakeWindowRootViewBlurRepository() }
+
+val Kosmos.windowRootViewBlurRepository: WindowRootViewBlurRepository by
+    Kosmos.Fixture { fakeWindowRootViewBlurRepository }
+
+class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository {
+    override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0)
+    override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false)
+}
diff --git a/services/accessibility/OWNERS b/services/accessibility/OWNERS
index 4e11750..ab1e9ff 100644
--- a/services/accessibility/OWNERS
+++ b/services/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 # Android Accessibility Framework owners
 danielnorman@google.com
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index e8dddcb5..529a564 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -100,6 +100,13 @@
 }
 
 flag {
+    name: "enable_low_vision_generic_feedback"
+    namespace: "accessibility"
+    description: "Use generic feedback for low vision."
+    bug: "393981463"
+}
+
+flag {
     name: "enable_low_vision_hats"
     namespace: "accessibility"
     description: "Use HaTS for low vision feedback."
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 8e44867..5283df5 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -17,11 +17,16 @@
 package com.android.server.accessibility.autoclick;
 
 import static android.view.MotionEvent.BUTTON_PRIMARY;
+import static android.view.MotionEvent.BUTTON_SECONDARY;
 import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
 import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
 import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
 
 import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface;
 
 import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.NonNull;
@@ -84,6 +89,23 @@
     @VisibleForTesting AutoclickTypePanel mAutoclickTypePanel;
     private WindowManager mWindowManager;
 
+    // Default click type is left-click.
+    private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK;
+
+    @VisibleForTesting
+    final ClickPanelControllerInterface clickPanelController =
+            new ClickPanelControllerInterface() {
+                @Override
+                public void handleAutoclickTypeChange(@AutoclickType int clickType) {
+                    mActiveClickType = clickType;
+                }
+
+                @Override
+                public void toggleAutoclickPause() {
+                    // TODO(b/388872274): allows users to pause the autoclick.
+                }
+            };
+
     public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
         mTrace = trace;
         mContext = context;
@@ -124,7 +146,8 @@
         mAutoclickIndicatorView = new AutoclickIndicatorView(mContext);
 
         mWindowManager = mContext.getSystemService(WindowManager.class);
-        mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager);
+        mAutoclickTypePanel =
+                new AutoclickTypePanel(mContext, mWindowManager, clickPanelController);
 
         mAutoclickTypePanel.show();
         mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
@@ -510,6 +533,11 @@
             return mMetaState;
         }
 
+        @VisibleForTesting
+        boolean getIsActiveForTesting() {
+            return mActive;
+        }
+
         /**
          * Updates delay that should be used when scheduling clicks. The delay will be used only for
          * clicks scheduled after this point (pending click tasks are not affected).
@@ -639,6 +667,15 @@
 
             final long now = SystemClock.uptimeMillis();
 
+            // TODO(b/395094903): always triggers left-click when the cursor hovers over the
+            // autoclick type panel, to always allow users to change a different click type.
+            // Otherwise, if one chooses the right-click, this user won't be able to rely on
+            // autoclick to select other click types.
+            final int actionButton =
+                    mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
+                            ? BUTTON_SECONDARY
+                            : BUTTON_PRIMARY;
+
             MotionEvent downEvent =
                     MotionEvent.obtain(
                             /* downTime= */ now,
@@ -648,7 +685,7 @@
                             mTempPointerProperties,
                             mTempPointerCoords,
                             mMetaState,
-                            BUTTON_PRIMARY,
+                            actionButton,
                             /* xPrecision= */ 1.0f,
                             /* yPrecision= */ 1.0f,
                             mLastMotionEvent.getDeviceId(),
@@ -658,11 +695,11 @@
 
             MotionEvent pressEvent = MotionEvent.obtain(downEvent);
             pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS);
-            pressEvent.setActionButton(BUTTON_PRIMARY);
+            pressEvent.setActionButton(actionButton);
 
             MotionEvent releaseEvent = MotionEvent.obtain(downEvent);
             releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE);
-            releaseEvent.setActionButton(BUTTON_PRIMARY);
+            releaseEvent.setActionButton(actionButton);
             releaseEvent.setButtonState(0);
 
             MotionEvent upEvent = MotionEvent.obtain(downEvent);
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
index 0354d2b..cf928e2 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
@@ -18,13 +18,17 @@
 
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import android.annotation.IntDef;
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.widget.ImageButton;
 import android.widget.LinearLayout;
 
 import androidx.annotation.NonNull;
@@ -36,12 +40,40 @@
 
     private final String TAG = AutoclickTypePanel.class.getSimpleName();
 
+    public static final int AUTOCLICK_TYPE_LEFT_CLICK = 0;
+    public static final int AUTOCLICK_TYPE_RIGHT_CLICK = 1;
+    public static final int AUTOCLICK_TYPE_DOUBLE_CLICK = 2;
+    public static final int AUTOCLICK_TYPE_DRAG = 3;
+    public static final int AUTOCLICK_TYPE_SCROLL = 4;
+
+    // Types of click the AutoclickTypePanel supports.
+    @IntDef({
+        AUTOCLICK_TYPE_LEFT_CLICK,
+        AUTOCLICK_TYPE_RIGHT_CLICK,
+        AUTOCLICK_TYPE_DOUBLE_CLICK,
+        AUTOCLICK_TYPE_DRAG,
+        AUTOCLICK_TYPE_SCROLL,
+    })
+    public @interface AutoclickType {}
+
+    // An interface exposed to {@link AutoclickController) to handle different actions on the panel,
+    // including changing autoclick type, pausing/resuming autoclick.
+    public interface ClickPanelControllerInterface {
+        // Allows users to change a different autoclick type.
+        void handleAutoclickTypeChange(@AutoclickType int clickType);
+
+        // Allows users to pause/resume the autoclick.
+        void toggleAutoclickPause();
+    }
+
     private final Context mContext;
 
     private final View mContentView;
 
     private final WindowManager mWindowManager;
 
+    private final ClickPanelControllerInterface mClickPanelController;
+
     // Whether the panel is expanded or not.
     private boolean mExpanded = false;
 
@@ -51,9 +83,15 @@
     private final LinearLayout mDragButton;
     private final LinearLayout mScrollButton;
 
-    public AutoclickTypePanel(Context context, WindowManager windowManager) {
+    private LinearLayout mSelectedButton;
+
+    public AutoclickTypePanel(
+            Context context,
+            WindowManager windowManager,
+            ClickPanelControllerInterface clickPanelController) {
         mContext = context;
         mWindowManager = windowManager;
+        mClickPanelController = clickPanelController;
 
         mContentView =
                 LayoutInflater.from(context)
@@ -71,15 +109,58 @@
     }
 
     private void initializeButtonState() {
-        mLeftClickButton.setOnClickListener(v -> togglePanelExpansion(mLeftClickButton));
-        mRightClickButton.setOnClickListener(v -> togglePanelExpansion(mRightClickButton));
-        mDoubleClickButton.setOnClickListener(v -> togglePanelExpansion(mDoubleClickButton));
-        mScrollButton.setOnClickListener(v -> togglePanelExpansion(mScrollButton));
-        mDragButton.setOnClickListener(v -> togglePanelExpansion(mDragButton));
+        mLeftClickButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_LEFT_CLICK));
+        mRightClickButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_RIGHT_CLICK));
+        mDoubleClickButton.setOnClickListener(
+                v -> togglePanelExpansion(AUTOCLICK_TYPE_DOUBLE_CLICK));
+        mScrollButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_SCROLL));
+        mDragButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_DRAG));
+
+        // TODO(b/388872274): registers listener for pause button and allows users to pause/resume
+        // the autoclick.
+        // TODO(b/388847771): registers listener for position button and allows users to move the
+        // panel to a different position.
 
         // Initializes panel as collapsed state and only displays the left click button.
         hideAllClickTypeButtons();
         mLeftClickButton.setVisibility(View.VISIBLE);
+        setSelectedClickType(AUTOCLICK_TYPE_LEFT_CLICK);
+    }
+
+    /** Sets the selected button and updates the newly and previously selected button styling. */
+    private void setSelectedClickType(@AutoclickType int clickType) {
+        final LinearLayout selectedButton = getButtonFromClickType(clickType);
+
+        // Updates the previously selected button styling.
+        if (mSelectedButton != null) {
+            toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false);
+        }
+
+        mSelectedButton = selectedButton;
+        mClickPanelController.handleAutoclickTypeChange(clickType);
+
+        // Updates the newly selected button styling.
+        toggleSelectedButtonStyle(selectedButton, /* isSelected= */ true);
+    }
+
+    private void toggleSelectedButtonStyle(@NonNull LinearLayout button, boolean isSelected) {
+        // Sets icon background color.
+        GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+        gradientDrawable.setColor(
+                mContext.getColor(
+                        isSelected
+                                ? R.color.materialColorPrimary
+                                : R.color.materialColorSurfaceContainer));
+
+        // Sets icon color.
+        ImageButton imageButton = (ImageButton) button.getChildAt(/* index= */ 0);
+        Drawable drawable = imageButton.getDrawable();
+        drawable.mutate()
+                .setTint(
+                        mContext.getColor(
+                                isSelected
+                                        ? R.color.materialColorSurfaceContainer
+                                        : R.color.materialColorPrimary));
     }
 
     public void show() {
@@ -91,12 +172,17 @@
     }
 
     /** Toggles the panel expanded or collapsed state. */
-    private void togglePanelExpansion(LinearLayout button) {
+    private void togglePanelExpansion(@AutoclickType int clickType) {
+        final LinearLayout button = getButtonFromClickType(clickType);
+
         if (mExpanded) {
             // If the panel is already in expanded state, we should collapse it by hiding all
             // buttons except the one user selected.
             hideAllClickTypeButtons();
             button.setVisibility(View.VISIBLE);
+
+            // Sets the newly selected button.
+            setSelectedClickType(clickType);
         } else {
             // If the panel is already collapsed, we just need to expand it.
             showAllClickTypeButtons();
@@ -124,6 +210,17 @@
         mScrollButton.setVisibility(View.VISIBLE);
     }
 
+    private LinearLayout getButtonFromClickType(@AutoclickType int clickType) {
+        return switch (clickType) {
+            case AUTOCLICK_TYPE_LEFT_CLICK -> mLeftClickButton;
+            case AUTOCLICK_TYPE_RIGHT_CLICK -> mRightClickButton;
+            case AUTOCLICK_TYPE_DOUBLE_CLICK -> mDoubleClickButton;
+            case AUTOCLICK_TYPE_DRAG -> mDragButton;
+            case AUTOCLICK_TYPE_SCROLL -> mScrollButton;
+            default -> throw new IllegalArgumentException("Unknown clickType " + clickType);
+        };
+    }
+
     @VisibleForTesting
     boolean getExpansionStateForTesting() {
         return mExpanded;
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
new file mode 100644
index 0000000..ff812ad
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 770744.
+
+juchengchou@google.com
+chenjean@google.com
+chihtinglo@google.com
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 4376444..d0ee7af 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -27,6 +27,7 @@
 import android.app.appfunctions.AppFunctionException;
 import android.app.appfunctions.AppFunctionManager;
 import android.app.appfunctions.AppFunctionManagerHelper;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
 import android.app.appfunctions.AppFunctionRuntimeMetadata;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
@@ -513,7 +514,9 @@
             e = e.getCause();
         }
         int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR;
-        if (e instanceof AppSearchException appSearchException) {
+        if (e instanceof AppFunctionNotFoundException) {
+            resultCode = AppFunctionException.ERROR_FUNCTION_NOT_FOUND;
+        } else if (e instanceof AppSearchException appSearchException) {
             resultCode =
                     mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(
                             appSearchException.getResultCode());
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 414db37..05301fd 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -588,10 +588,10 @@
         @Override
         @EnforcePermission(DELIVER_COMPANION_MESSAGES)
         public void attachSystemDataTransport(String packageName, int userId, int associationId,
-                ParcelFileDescriptor fd) {
+                                              ParcelFileDescriptor fd, int flags) {
             attachSystemDataTransport_enforcePermission();
 
-            mTransportManager.attachSystemDataTransport(associationId, fd);
+            mTransportManager.attachSystemDataTransport(associationId, fd, flags);
         }
 
         @Override
diff --git a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java
index df3071e..42af059 100644
--- a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java
+++ b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java
@@ -16,7 +16,9 @@
 
 package com.android.server.companion.securechannel;
 
+import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF;
 import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE;
+import static android.security.attestationverification.AttestationVerificationManager.PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS;
 import static android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE;
 import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE;
 
@@ -34,15 +36,21 @@
 
 /**
  * Helper class to perform attestation verification synchronously.
+ *
+ * @hide
  */
 public class AttestationVerifier {
     private static final long ATTESTATION_VERIFICATION_TIMEOUT_SECONDS = 10; // 10 seconds
     private static final String PARAM_OWNED_BY_SYSTEM = "android.key_owned_by_system";
 
-    private final Context mContext;
+    private static final int EXTENDED_PATCH_LEVEL_DIFF_MONTHS = 24; // 2 years
 
-    AttestationVerifier(Context context) {
+    private final Context mContext;
+    private final int mFlags;
+
+    AttestationVerifier(Context context, int flags) {
         this.mContext = context;
+        this.mFlags = flags;
     }
 
     /**
@@ -59,10 +67,13 @@
             @NonNull byte[] remoteAttestation,
             @NonNull byte[] attestationChallenge
     ) throws SecureChannelException {
-        Bundle requirements = new Bundle();
+        final Bundle requirements = new Bundle();
         requirements.putByteArray(PARAM_CHALLENGE, attestationChallenge);
         requirements.putBoolean(PARAM_OWNED_BY_SYSTEM, true); // Custom parameter for CDM
 
+        // Apply flags to verifier requirements
+        updateRequirements(requirements);
+
         // Synchronously execute attestation verification.
         AtomicInteger verificationResult = new AtomicInteger(0);
         CountDownLatch verificationFinished = new CountDownLatch(1);
@@ -96,4 +107,15 @@
 
         return verificationResult.get();
     }
+
+    private void updateRequirements(Bundle requirements) {
+        if (mFlags == 0) {
+            return;
+        }
+
+        if ((mFlags & TRANSPORT_FLAG_EXTEND_PATCH_DIFF) > 0) {
+            requirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS,
+                    EXTENDED_PATCH_LEVEL_DIFF_MONTHS);
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
index 2d3782f..6c7c9b3 100644
--- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
+++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
@@ -59,6 +59,7 @@
     private final Callback mCallback;
     private final byte[] mPreSharedKey;
     private final AttestationVerifier mVerifier;
+    private final int mFlags;
 
     private volatile boolean mStopped;
     private volatile boolean mInProgress;
@@ -89,7 +90,7 @@
             @NonNull Callback callback,
             @NonNull byte[] preSharedKey
     ) {
-        this(in, out, callback, preSharedKey, null);
+        this(in, out, callback, preSharedKey, null, 0);
     }
 
     /**
@@ -100,14 +101,16 @@
      * @param out output stream from which data is sent out
      * @param callback subscription to received messages from the channel
      * @param context context for fetching the Attestation Verifier Framework system service
+     * @param flags flags for custom security settings on the channel
      */
     public SecureChannel(
             @NonNull final InputStream in,
             @NonNull final OutputStream out,
             @NonNull Callback callback,
-            @NonNull Context context
+            @NonNull Context context,
+            int flags
     ) {
-        this(in, out, callback, null, new AttestationVerifier(context));
+        this(in, out, callback, null, new AttestationVerifier(context, flags), flags);
     }
 
     public SecureChannel(
@@ -115,13 +118,15 @@
             final OutputStream out,
             Callback callback,
             byte[] preSharedKey,
-            AttestationVerifier verifier
+            AttestationVerifier verifier,
+            int flags
     ) {
         this.mInput = in;
         this.mOutput = out;
         this.mCallback = callback;
         this.mPreSharedKey = preSharedKey;
         this.mVerifier = verifier;
+        this.mFlags = flags;
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 3608360..92d9fb0 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -16,7 +16,11 @@
 
 package com.android.server.companion.transport;
 
+import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING;
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PERMISSION_RESTORE;
+import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF;
+
+import static com.android.server.companion.transport.TransportUtils.enforceAssociationCanUseTransportFlags;
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
@@ -152,10 +156,14 @@
     /**
      * Attach transport.
      */
-    public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) {
+    public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd,
+                                          int flags) {
         Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]...");
 
-        mAssociationStore.getAssociationWithCallerChecks(associationId);
+        AssociationInfo association =
+                mAssociationStore.getAssociationWithCallerChecks(associationId);
+
+        enforceAssociationCanUseTransportFlags(association, flags);
 
         synchronized (mTransports) {
             if (mTransports.contains(associationId)) {
@@ -163,7 +171,7 @@
             }
 
             // TODO: Implement new API to pass a PSK
-            initializeTransport(associationId, fd, null);
+            initializeTransport(association, fd, null, flags);
 
             notifyOnTransportsChanged();
         }
@@ -217,10 +225,12 @@
         }
     }
 
-    private void initializeTransport(int associationId,
+    private void initializeTransport(AssociationInfo association,
                                      ParcelFileDescriptor fd,
-                                     byte[] preSharedKey) {
+                                     byte[] preSharedKey,
+                                     int flags) {
         Slog.i(TAG, "Initializing transport");
+        int associationId = association.getId();
         Transport transport;
         if (!isSecureTransportEnabled()) {
             // If secure transport is explicitly disabled for testing, use raw transport
@@ -230,15 +240,21 @@
             // If device is debug build, use hardcoded test key for authentication
             Slog.d(TAG, "Creating an unauthenticated secure channel");
             final byte[] testKey = "CDM".getBytes(StandardCharsets.UTF_8);
-            transport = new SecureTransport(associationId, fd, mContext, testKey, null);
+            transport = new SecureTransport(associationId, fd, mContext, testKey, null, 0);
         } else if (preSharedKey != null) {
             // If either device is not Android, then use app-specific pre-shared key
             Slog.d(TAG, "Creating a PSK-authenticated secure channel");
-            transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null);
+            transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null, 0);
+        } else if (DEVICE_PROFILE_WEARABLE_SENSING.equals(association.getDeviceProfile())) {
+            // If device is glasses with WEARABLE_SENSING profile, extend the allowed patch
+            // difference to 2 years instead of 1.
+            Slog.d(TAG, "Creating a secure channel with extended patch difference allowance");
+            transport = new SecureTransport(associationId, fd, mContext,
+                    TRANSPORT_FLAG_EXTEND_PATCH_DIFF);
         } else {
             // If none of the above applies, then use secure channel with attestation verification
             Slog.d(TAG, "Creating a secure channel");
-            transport = new SecureTransport(associationId, fd, mContext);
+            transport = new SecureTransport(associationId, fd, mContext, flags);
         }
 
         addMessageListenersToTransport(transport);
diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
index 1e95e65..77dc809 100644
--- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java
+++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
@@ -36,15 +36,22 @@
 
     private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500);
 
-    SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) {
+    SecureTransport(int associationId, ParcelFileDescriptor fd, Context context, int flags) {
         super(associationId, fd, context);
-        mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context);
+        mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context, flags);
     }
 
     SecureTransport(int associationId, ParcelFileDescriptor fd, Context context,
-            byte[] preSharedKey, AttestationVerifier verifier) {
+            byte[] preSharedKey, AttestationVerifier verifier, int flags) {
         super(associationId, fd, context);
-        mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, preSharedKey, verifier);
+        mSecureChannel = new SecureChannel(
+                mRemoteIn,
+                mRemoteOut,
+                this,
+                preSharedKey,
+                verifier,
+                flags
+        );
     }
 
     @Override
diff --git a/services/companion/java/com/android/server/companion/transport/TransportUtils.java b/services/companion/java/com/android/server/companion/transport/TransportUtils.java
new file mode 100644
index 0000000..7a15c11
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/transport/TransportUtils.java
@@ -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.server.companion.transport;
+
+import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING;
+import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF;
+
+import static java.util.Collections.unmodifiableMap;
+
+import android.companion.AssociationInfo;
+import android.util.ArrayMap;
+
+import java.util.Map;
+
+/**
+ * Utility class for transport manager.
+ * @hide
+ */
+public final class TransportUtils {
+
+    /**
+     * Device profile -> Union of allowlisted transport flags
+     */
+    private static final Map<String, Integer> DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST;
+    static {
+        final Map<String, Integer> map = new ArrayMap<>();
+        map.put(DEVICE_PROFILE_WEARABLE_SENSING,
+                TRANSPORT_FLAG_EXTEND_PATCH_DIFF);
+        DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST = unmodifiableMap(map);
+    }
+
+    /**
+     * Enforce that the association that is trying to attach a transport with provided flags has
+     * one of the allowlisted device profiles that may apply the flagged features.
+     *
+     * @param association Association for which transport is being attached
+     * @param flags Flags for features being applied to the transport
+     */
+    public static void enforceAssociationCanUseTransportFlags(
+            AssociationInfo association, int flags) {
+        if (flags == 0) {
+            return;
+        }
+
+        final String deviceProfile = association.getDeviceProfile();
+        if (!DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.containsKey(deviceProfile)) {
+            throw new IllegalArgumentException("Association (id=" + association.getId()
+                    + ") with device profile " + deviceProfile + " does not support the "
+                    + "usage of transport flags.");
+        }
+
+        int allowedFlags = DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.get(deviceProfile);
+
+        // Ensure that every non-zero bits in flags are also present in allowed flags
+        if ((allowedFlags & flags) != flags) {
+            throw new IllegalArgumentException("Association (id=" + association.getId()
+                    + ") does not have the device profile required to use at least "
+                    + "one of the flags in this transport.");
+        }
+    }
+
+    private TransportUtils() {}
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index b75728e..f03e8c7 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -110,6 +110,7 @@
 import android.view.Display;
 import android.view.WindowManager;
 import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -1411,8 +1412,7 @@
         return mirroredDisplayId == Display.INVALID_DISPLAY ? displayId : mirroredDisplayId;
     }
 
-    @GuardedBy("mVirtualDeviceLock")
-    private GenericWindowPolicyController createWindowPolicyControllerLocked(
+    private GenericWindowPolicyController createWindowPolicyController(
             @NonNull Set<String> displayCategories) {
         final boolean activityLaunchAllowedByDefault =
                 getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT;
@@ -1421,28 +1421,28 @@
         final boolean showTasksInHostDeviceRecents =
                 getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
 
-        if (mActivityListenerAdapter == null) {
-            mActivityListenerAdapter = new GwpcActivityListener();
-        }
+        synchronized (mVirtualDeviceLock) {
+            if (mActivityListenerAdapter == null) {
+                mActivityListenerAdapter = new GwpcActivityListener();
+            }
 
-        final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
-                WindowManager.LayoutParams.FLAG_SECURE,
-                WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
-                mAttributionSource,
-                getAllowedUserHandles(),
-                activityLaunchAllowedByDefault,
-                mActivityPolicyExemptions,
-                mActivityPolicyPackageExemptions,
-                crossTaskNavigationAllowedByDefault,
-                /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
-                        ? mParams.getBlockedCrossTaskNavigations()
-                        : mParams.getAllowedCrossTaskNavigations(),
-                mActivityListenerAdapter,
-                displayCategories,
-                showTasksInHostDeviceRecents,
-                mParams.getHomeComponent());
-        gwpc.registerRunningAppsChangedListener(/* listener= */ this);
-        return gwpc;
+            return new GenericWindowPolicyController(
+                    WindowManager.LayoutParams.FLAG_SECURE,
+                    WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+                    mAttributionSource,
+                    getAllowedUserHandles(),
+                    activityLaunchAllowedByDefault,
+                    mActivityPolicyExemptions,
+                    mActivityPolicyPackageExemptions,
+                    crossTaskNavigationAllowedByDefault,
+                    /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
+                            ? mParams.getBlockedCrossTaskNavigations()
+                            : mParams.getAllowedCrossTaskNavigations(),
+                    mActivityListenerAdapter,
+                    displayCategories,
+                    showTasksInHostDeviceRecents,
+                    mParams.getHomeComponent());
+        }
     }
 
     @Override // Binder call
@@ -1450,55 +1450,54 @@
             @NonNull IVirtualDisplayCallback callback) {
         checkCallerIsDeviceOwner();
 
-        int displayId;
-        boolean showPointer;
-        boolean isTrustedDisplay;
-        GenericWindowPolicyController gwpc;
-        synchronized (mVirtualDeviceLock) {
-            gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
-            displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
-                    callback, this, gwpc, mOwnerPackageName);
-            boolean isMirrorDisplay =
-                    mDisplayManagerInternal.getDisplayIdToMirror(displayId)
-                            != Display.INVALID_DISPLAY;
-            gwpc.setDisplayId(displayId, isMirrorDisplay);
-            isTrustedDisplay =
-                    (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
-                            == Display.FLAG_TRUSTED;
-            if (!isTrustedDisplay
-                    && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
-                throw new SecurityException("All displays must be trusted for devices with "
-                        + "custom clipboard policy.");
-            }
-
-            if (mVirtualDisplays.contains(displayId)) {
-                gwpc.unregisterRunningAppsChangedListener(this);
-                throw new IllegalStateException(
-                        "Virtual device already has a virtual display with ID " + displayId);
-            }
-
-            PowerManager.WakeLock wakeLock =
-                    isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
-            mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
-                    isTrustedDisplay, isMirrorDisplay));
-            showPointer = mDefaultShowPointerIcon;
+        final boolean isTrustedDisplay =
+                (virtualDisplayConfig.getFlags() & DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED)
+                        == DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+        if (!isTrustedDisplay && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+            throw new SecurityException(
+                "All displays must be trusted for devices with custom clipboard policy.");
         }
 
-        final long token = Binder.clearCallingIdentity();
-        try {
+        GenericWindowPolicyController gwpc =
+                createWindowPolicyController(virtualDisplayConfig.getDisplayCategories());
+
+        // Create the display outside of the lock to avoid deadlock. DisplayManagerService will
+        // acquire the global WM lock while creating the display. At the same time, WM may query
+        // VDM and this virtual device to get policies, display ownership, etc.
+        int displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
+                    callback, this, gwpc, mOwnerPackageName);
+        if (displayId == Display.INVALID_DISPLAY) {
+            return displayId;
+        }
+
+        // DisplayManagerService will call onVirtualDisplayCreated() after the display is created,
+        // while holding its own lock to ensure that this device knows about the display before any
+        // other display listeners are notified about the display creation.
+        VirtualDisplayWrapper displayWrapper;
+        boolean showPointer;
+        synchronized (mVirtualDeviceLock) {
+            if (!mVirtualDisplays.contains(displayId)) {
+                throw new IllegalStateException("Virtual device was not notified about the "
+                        + "creation of display with ID " + displayId);
+            }
+            displayWrapper = mVirtualDisplays.get(displayId);
+            showPointer = mDefaultShowPointerIcon;
+        }
+        displayWrapper.acquireWakeLock();
+        gwpc.registerRunningAppsChangedListener(/* listener= */ this);
+
+        Binder.withCleanCallingIdentity(() -> {
             mInputController.setMouseScalingEnabled(false, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
-            if (isTrustedDisplay) {
+            if (displayWrapper.isTrusted()) {
                 mInputController.setShowPointerIcon(showPointer, displayId);
                 mInputController.setDisplayImePolicy(displayId,
                         WindowManager.DISPLAY_IME_POLICY_LOCAL);
             } else {
                 gwpc.setShowInHostDeviceRecents(true);
             }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
+        });
 
         Counter.logIncrementWithUid(
                 "virtual_devices.value_virtual_display_created_count",
@@ -1506,7 +1505,7 @@
         return displayId;
     }
 
-    private PowerManager.WakeLock createAndAcquireWakeLockForDisplay(int displayId) {
+    private PowerManager.WakeLock createWakeLockForDisplay(int displayId) {
         if (Flags.deviceAwareDisplayPower()) {
             return null;
         }
@@ -1516,7 +1515,6 @@
             PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
                     PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
                     TAG + ":" + displayId, displayId);
-            wakeLock.acquire();
             return wakeLock;
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -1561,17 +1559,47 @@
         return result;
     }
 
+    /**
+     * DisplayManagerService is notifying this virtual device about the display creation. This
+     * should happen before the DisplayManagerInternal#createVirtualDisplay() call above
+     * returns.
+     * This is called while holding the DisplayManagerService lock, so no heavy-weight work must
+     * be done here and especially *** no calls to WindowManager! ***
+     */
+    public void onVirtualDisplayCreated(int displayId, IVirtualDisplayCallback callback,
+            DisplayWindowPolicyController dwpc) {
+        final boolean isMirrorDisplay =
+                mDisplayManagerInternal.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY;
+        final boolean isTrustedDisplay =
+                (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
+                        == Display.FLAG_TRUSTED;
+
+        GenericWindowPolicyController gwpc = (GenericWindowPolicyController) dwpc;
+        gwpc.setDisplayId(displayId, isMirrorDisplay);
+        PowerManager.WakeLock wakeLock =
+                isTrustedDisplay ? createWakeLockForDisplay(displayId) : null;
+        synchronized (mVirtualDeviceLock) {
+            if (mVirtualDisplays.contains(displayId)) {
+                Slog.wtf(TAG, "Virtual device already has a virtual display with ID " + displayId);
+                return;
+            }
+            mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
+                    isTrustedDisplay, isMirrorDisplay));
+        }
+    }
+
+    /**
+     * This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
+     * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
+     * At this point, the display is already released, but we still need to release the
+     * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
+     * WindowPolicyController.
+     *
+     * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
+     * this callback won't be invoked because the display is removed from
+     * VirtualDeviceManagerService before any resources are released.
+     */
     void onVirtualDisplayRemoved(int displayId) {
-        /* This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
-         * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
-         * At this point, the display is already released, but we still need to release the
-         * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
-         * WindowPolicyController.
-         *
-         * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
-         * this callback won't be invoked because the display is removed from
-         * VirtualDeviceManagerService before any resources are released.
-         */
         VirtualDisplayWrapper virtualDisplayWrapper;
         synchronized (mVirtualDeviceLock) {
             virtualDisplayWrapper = mVirtualDisplays.removeReturnOld(displayId);
@@ -1847,6 +1875,12 @@
             return mWindowPolicyController;
         }
 
+        void acquireWakeLock() {
+            if (mWakeLock != null && !mWakeLock.isHeld()) {
+                mWakeLock.acquire();
+            }
+        }
+
         void releaseWakeLock() {
             if (mWakeLock != null && mWakeLock.isHeld()) {
                 mWakeLock.release();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 8a0b8585..ff82ca0 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -40,7 +40,6 @@
 import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.VirtualDeviceManager;
 import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtualnative.IVirtualDeviceManagerNative;
 import android.compat.annotation.ChangeId;
@@ -49,6 +48,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.IVirtualDisplayCallback;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -67,6 +67,7 @@
 import android.util.SparseArray;
 import android.view.Display;
 import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -751,6 +752,16 @@
         }
 
         @Override
+        public void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+                IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc) {
+            VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
+                    ((VirtualDeviceImpl) virtualDevice).getDeviceId());
+            if (virtualDeviceImpl != null) {
+                virtualDeviceImpl.onVirtualDisplayCreated(displayId, callback, dwpc);
+            }
+        }
+
+        @Override
         public void onVirtualDisplayRemoved(IVirtualDevice virtualDevice, int displayId) {
             VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
                     ((VirtualDeviceImpl) virtualDevice).getDeviceId());
diff --git a/services/core/Android.bp b/services/core/Android.bp
index f98076a..00db11e 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -292,9 +292,18 @@
     out: ["services.core.priorityboosted.jar"],
 }
 
+java_genrule_combiner {
+    name: "services.core.combined",
+    static_libs: ["services.core.priorityboosted"],
+    headers: ["services.core.unboosted"],
+}
+
 java_library {
     name: "services.core",
-    static_libs: ["services.core.priorityboosted"],
+    static_libs: select(release_flag("RELEASE_SERVICES_JAVA_GENRULE_COMBINER"), {
+        true: ["services.core.combined"],
+        default: ["services.core.priorityboosted"],
+    }),
 }
 
 java_library_host {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 350ecab..d2a5734 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -163,6 +163,10 @@
 import com.android.server.storage.AppFuseBridge;
 import com.android.server.storage.StorageSessionController;
 import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
+import com.android.server.storage.WatchedVolumeInfo;
+import com.android.server.utils.Watchable;
+import com.android.server.utils.WatchedArrayMap;
+import com.android.server.utils.Watcher;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver;
 
@@ -452,7 +456,7 @@
     private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>();
     /** Map from volume ID to disk */
     @GuardedBy("mLock")
-    private final ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>();
+    private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>();
 
     /** Map from UUID to record */
     @GuardedBy("mLock")
@@ -503,9 +507,9 @@
             "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?");
 
 
-    private VolumeInfo findVolumeByIdOrThrow(String id) {
+    private WatchedVolumeInfo findVolumeByIdOrThrow(String id) {
         synchronized (mLock) {
-            final VolumeInfo vol = mVolumes.get(id);
+            final WatchedVolumeInfo vol = mVolumes.get(id);
             if (vol != null) {
                 return vol;
             }
@@ -516,9 +520,9 @@
     private VolumeRecord findRecordForPath(String path) {
         synchronized (mLock) {
             for (int i = 0; i < mVolumes.size(); i++) {
-                final VolumeInfo vol = mVolumes.valueAt(i);
-                if (vol.path != null && path.startsWith(vol.path)) {
-                    return mRecords.get(vol.fsUuid);
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+                if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) {
+                    return mRecords.get(vol.getFsUuid());
                 }
             }
         }
@@ -764,7 +768,7 @@
                     break;
                 }
                 case H_VOLUME_MOUNT: {
-                    final VolumeInfo vol = (VolumeInfo) msg.obj;
+                    final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
                     if (isMountDisallowed(vol)) {
                         Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy");
                         break;
@@ -774,7 +778,7 @@
                     break;
                 }
                 case H_VOLUME_UNMOUNT: {
-                    final VolumeInfo vol = (VolumeInfo) msg.obj;
+                    final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
                     unmount(vol);
                     break;
                 }
@@ -828,7 +832,8 @@
                 }
                 case H_VOLUME_STATE_CHANGED: {
                     final SomeArgs args = (SomeArgs) msg.obj;
-                    onVolumeStateChangedAsync((VolumeInfo) args.arg1, args.argi1, args.argi2);
+                    onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1,
+                            args.argi2);
                     args.recycle();
                     break;
                 }
@@ -892,9 +897,9 @@
                     synchronized (mLock) {
                         final int size = mVolumes.size();
                         for (int i = 0; i < size; i++) {
-                            final VolumeInfo vol = mVolumes.valueAt(i);
-                            if (vol.mountUserId == userId) {
-                                vol.mountUserId = UserHandle.USER_NULL;
+                            final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+                            if (vol.getMountUserId() == userId) {
+                                vol.setMountUserId(UserHandle.USER_NULL);
                                 mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
                             }
                         }
@@ -1084,7 +1089,7 @@
                 VolumeInfo.TYPE_PRIVATE, null, null);
         internal.state = VolumeInfo.STATE_MOUNTED;
         internal.path = Environment.getDataDirectory().getAbsolutePath();
-        mVolumes.put(internal.id, internal);
+        mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal));
     }
 
     private void resetIfBootedAndConnected() {
@@ -1242,7 +1247,7 @@
                 }
             }
             for (int i = 0; i < mVolumes.size(); i++) {
-                final VolumeInfo vol = mVolumes.valueAt(i);
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                 if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) {
                     final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false);
                     mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();
@@ -1291,21 +1296,21 @@
     }
 
     private void maybeRemountVolumes(int userId) {
-        List<VolumeInfo> volumesToRemount = new ArrayList<>();
+        List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
         synchronized (mLock) {
             for (int i = 0; i < mVolumes.size(); i++) {
-                final VolumeInfo vol = mVolumes.valueAt(i);
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                 if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible()
                         && vol.getMountUserId() != mCurrentUserId) {
                     // If there's a visible secondary volume mounted,
                     // we need to update the currentUserId and remount
-                    vol.mountUserId = mCurrentUserId;
+                    vol.setMountUserId(mCurrentUserId);
                     volumesToRemount.add(vol);
                 }
             }
         }
 
-        for (VolumeInfo vol : volumesToRemount) {
+        for (WatchedVolumeInfo vol : volumesToRemount) {
             Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
             mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
@@ -1317,12 +1322,12 @@
      * trying to mount doesn't have the same mount user id as the current user being maintained by
      * StorageManagerService and change the mount Id. The checks are same as
      * {@link StorageManagerService#maybeRemountVolumes(int)}
-     * @param VolumeInfo object to consider for changing the mountId
+     * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId
      */
-    private void updateVolumeMountIdIfRequired(VolumeInfo vol) {
+    private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) {
         synchronized (mLock) {
             if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) {
-                vol.mountUserId = mCurrentUserId;
+                vol.setMountUserId(mCurrentUserId);
             }
         }
     }
@@ -1485,20 +1490,21 @@
                 final DiskInfo disk = mDisks.get(diskId);
                 final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid);
                 vol.mountUserId = userId;
-                mVolumes.put(volId, vol);
-                onVolumeCreatedLocked(vol);
+                WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol);
+                mVolumes.put(volId, watchedVol);
+                onVolumeCreatedLocked(watchedVol);
             }
         }
 
         @Override
         public void onVolumeStateChanged(String volId, final int newState, final int userId) {
             synchronized (mLock) {
-                final VolumeInfo vol = mVolumes.get(volId);
+                final WatchedVolumeInfo vol = mVolumes.get(volId);
                 if (vol != null) {
-                    final int oldState = vol.state;
-                    vol.state = newState;
-                    final VolumeInfo vInfo = new VolumeInfo(vol);
-                    vInfo.mountUserId = userId;
+                    final int oldState = vol.getState();
+                    vol.setState(newState);
+                    final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol);
+                    vInfo.setMountUserId(userId);
                     final SomeArgs args = SomeArgs.obtain();
                     args.arg1 = vInfo;
                     args.argi1 = oldState;
@@ -1513,11 +1519,11 @@
         public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid,
                 String fsLabel) {
             synchronized (mLock) {
-                final VolumeInfo vol = mVolumes.get(volId);
+                final WatchedVolumeInfo vol = mVolumes.get(volId);
                 if (vol != null) {
-                    vol.fsType = fsType;
-                    vol.fsUuid = fsUuid;
-                    vol.fsLabel = fsLabel;
+                    vol.setFsType(fsType);
+                    vol.setFsUuid(fsUuid);
+                    vol.setFsLabel(fsLabel);
                 }
             }
         }
@@ -1525,9 +1531,9 @@
         @Override
         public void onVolumePathChanged(String volId, String path) {
             synchronized (mLock) {
-                final VolumeInfo vol = mVolumes.get(volId);
+                final WatchedVolumeInfo vol = mVolumes.get(volId);
                 if (vol != null) {
-                    vol.path = path;
+                    vol.setFsPath(path);
                 }
             }
         }
@@ -1535,24 +1541,24 @@
         @Override
         public void onVolumeInternalPathChanged(String volId, String internalPath) {
             synchronized (mLock) {
-                final VolumeInfo vol = mVolumes.get(volId);
+                final WatchedVolumeInfo vol = mVolumes.get(volId);
                 if (vol != null) {
-                    vol.internalPath = internalPath;
+                    vol.setInternalPath(internalPath);
                 }
             }
         }
 
         @Override
         public void onVolumeDestroyed(String volId) {
-            VolumeInfo vol = null;
+            WatchedVolumeInfo vol = null;
             synchronized (mLock) {
                 vol = mVolumes.remove(volId);
             }
 
             if (vol != null) {
-                mStorageSessionController.onVolumeRemove(vol);
+                mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo());
                 try {
-                    if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+                    if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
                         mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
                     }
                 } catch (Installer.InstallerException e) {
@@ -1566,7 +1572,7 @@
     private void onDiskScannedLocked(DiskInfo disk) {
         int volumeCount = 0;
         for (int i = 0; i < mVolumes.size(); i++) {
-            final VolumeInfo vol = mVolumes.valueAt(i);
+            final WatchedVolumeInfo vol = mVolumes.valueAt(i);
             if (Objects.equals(disk.id, vol.getDiskId())) {
                 volumeCount++;
             }
@@ -1589,19 +1595,19 @@
     }
 
     @GuardedBy("mLock")
-    private void onVolumeCreatedLocked(VolumeInfo vol) {
+    private void onVolumeCreatedLocked(WatchedVolumeInfo vol) {
         final ActivityManagerInternal amInternal =
                 LocalServices.getService(ActivityManagerInternal.class);
 
-        if (vol.mountUserId >= 0 && !amInternal.isUserRunning(vol.mountUserId, 0)) {
+        if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) {
             Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
-                    + Integer.toString(vol.mountUserId) + " is no longer running.");
+                    + Integer.toString(vol.getMountUserId()) + " is no longer running.");
             return;
         }
 
-        if (vol.type == VolumeInfo.TYPE_EMULATED) {
+        if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
             final Context volumeUserContext = mContext.createContextAsUser(
-                    UserHandle.of(vol.mountUserId), 0);
+                    UserHandle.of(vol.getMountUserId()), 0);
 
             boolean isMediaSharedWithParent =
                     (volumeUserContext != null) ? volumeUserContext.getSystemService(
@@ -1611,60 +1617,60 @@
             // should not be skipped even if media provider instance is not running in that user
             // space
             if (!isMediaSharedWithParent
-                    && !mStorageSessionController.supportsExternalStorage(vol.mountUserId)) {
+                    && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) {
                 Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
-                        + Integer.toString(vol.mountUserId)
+                        + Integer.toString(vol.getMountUserId())
                         + " does not support external storage.");
                 return;
             }
 
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
-            final VolumeInfo privateVol = storage.findPrivateForEmulated(vol);
+            final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo());
 
             if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
                     && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id))
-                    || Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {
+                    || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) {
                 Slog.v(TAG, "Found primary storage at " + vol);
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
                 mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
             }
 
-        } else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
+        } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
             // TODO: only look at first public partition
             if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
-                    && vol.disk.isDefaultPrimary()) {
+                    && vol.getDisk().isDefaultPrimary()) {
                 Slog.v(TAG, "Found primary storage at " + vol);
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
             }
 
             // Adoptable public disks are visible to apps, since they meet
             // public API requirement of being in a stable location.
-            if (vol.disk.isAdoptable()) {
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+            if (vol.getDisk().isAdoptable()) {
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
             }
 
-            vol.mountUserId = mCurrentUserId;
+            vol.setMountUserId(mCurrentUserId);
             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
 
-        } else if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+        } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
 
-        } else if (vol.type == VolumeInfo.TYPE_STUB) {
-            if (vol.disk.isStubVisible()) {
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+        } else if (vol.getType() == VolumeInfo.TYPE_STUB) {
+            if (vol.getDisk().isStubVisible()) {
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
             } else {
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ;
+                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ);
             }
-            vol.mountUserId = mCurrentUserId;
+            vol.setMountUserId(mCurrentUserId);
             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
         } else {
             Slog.d(TAG, "Skipping automatic mounting of " + vol);
         }
     }
 
-    private boolean isBroadcastWorthy(VolumeInfo vol) {
+    private boolean isBroadcastWorthy(WatchedVolumeInfo vol) {
         switch (vol.getType()) {
             case VolumeInfo.TYPE_PRIVATE:
             case VolumeInfo.TYPE_PUBLIC:
@@ -1691,8 +1697,8 @@
     }
 
     @GuardedBy("mLock")
-    private void onVolumeStateChangedLocked(VolumeInfo vol, int newState) {
-        if (vol.type == VolumeInfo.TYPE_EMULATED) {
+    private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) {
+        if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
             if (newState != VolumeInfo.STATE_MOUNTED) {
                 mFuseMountedUser.remove(vol.getMountUserId());
             } else if (mVoldAppDataIsolationEnabled){
@@ -1741,7 +1747,7 @@
         }
     }
 
-    private void onVolumeStateChangedAsync(VolumeInfo vol, int oldState, int newState) {
+    private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) {
         if (newState == VolumeInfo.STATE_MOUNTED) {
             // Private volumes can be unmounted and re-mounted even after a user has
             // been unlocked; on devices that support encryption keys tied to the filesystem,
@@ -1751,7 +1757,7 @@
             } catch (Exception e) {
                 // Unusable partition, unmount.
                 try {
-                    mVold.unmount(vol.id);
+                    mVold.unmount(vol.getId());
                 } catch (Exception ee) {
                     Slog.wtf(TAG, ee);
                 }
@@ -1762,20 +1768,20 @@
         synchronized (mLock) {
             // Remember that we saw this volume so we're ready to accept user
             // metadata, or so we can annoy them when a private volume is ejected
-            if (!TextUtils.isEmpty(vol.fsUuid)) {
-                VolumeRecord rec = mRecords.get(vol.fsUuid);
+            if (!TextUtils.isEmpty(vol.getFsUuid())) {
+                VolumeRecord rec = mRecords.get(vol.getFsUuid());
                 if (rec == null) {
-                    rec = new VolumeRecord(vol.type, vol.fsUuid);
-                    rec.partGuid = vol.partGuid;
+                    rec = new VolumeRecord(vol.getType(), vol.getFsUuid());
+                    rec.partGuid = vol.getPartGuid();
                     rec.createdMillis = System.currentTimeMillis();
-                    if (vol.type == VolumeInfo.TYPE_PRIVATE) {
-                        rec.nickname = vol.disk.getDescription();
+                    if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
+                        rec.nickname = vol.getDisk().getDescription();
                     }
                     mRecords.put(rec.fsUuid, rec);
                 } else {
                     // Handle upgrade case where we didn't store partition GUID
                     if (TextUtils.isEmpty(rec.partGuid)) {
-                        rec.partGuid = vol.partGuid;
+                        rec.partGuid = vol.getPartGuid();
                     }
                 }
 
@@ -1788,7 +1794,7 @@
         // before notifying other listeners.
         // Intentionally called without the mLock to avoid deadlocking from the Storage Service.
         try {
-            mStorageSessionController.notifyVolumeStateChanged(vol);
+            mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo());
         } catch (ExternalStorageServiceException e) {
             Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e);
         }
@@ -1799,9 +1805,9 @@
             // processes that receive the intent unnecessarily.
             if (mBootCompleted && isBroadcastWorthy(vol)) {
                 final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED);
-                intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
+                intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
                 intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState);
-                intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid);
+                intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
                 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                 mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();
@@ -1826,8 +1832,8 @@
                 }
             }
 
-            if ((vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_STUB)
-                    && vol.state == VolumeInfo.STATE_EJECTING) {
+            if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB)
+                    && vol.getState() == VolumeInfo.STATE_EJECTING) {
                 // TODO: this should eventually be handled by new ObbVolume state changes
                 /*
                  * Some OBBs might have been unmounted when this volume was
@@ -1835,7 +1841,7 @@
                  * remove those from the list of mounted OBBS.
                  */
                 mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(
-                        OBB_FLUSH_MOUNT_STATE, vol.path));
+                        OBB_FLUSH_MOUNT_STATE, vol.getFsPath()));
             }
             maybeLogMediaMount(vol, newState);
         }
@@ -1860,7 +1866,7 @@
         }
     }
 
-    private void maybeLogMediaMount(VolumeInfo vol, int newState) {
+    private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) {
         if (!SecurityLog.isLoggingEnabled()) {
             return;
         }
@@ -1875,10 +1881,10 @@
 
         if (newState == VolumeInfo.STATE_MOUNTED
                 || newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) {
-            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.path, label);
+            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label);
         } else if (newState == VolumeInfo.STATE_UNMOUNTED
                 || newState == VolumeInfo.STATE_BAD_REMOVAL) {
-            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.path, label);
+            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label);
         }
     }
 
@@ -1920,18 +1926,18 @@
     /**
      * Decide if volume is mountable per device policies.
      */
-    private boolean isMountDisallowed(VolumeInfo vol) {
+    private boolean isMountDisallowed(WatchedVolumeInfo vol) {
         UserManager userManager = mContext.getSystemService(UserManager.class);
 
         boolean isUsbRestricted = false;
-        if (vol.disk != null && vol.disk.isUsb()) {
+        if (vol.getDisk() != null && vol.getDisk().isUsb()) {
             isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER,
                     Binder.getCallingUserHandle());
         }
 
         boolean isTypeRestricted = false;
-        if (vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_PRIVATE
-                || vol.type == VolumeInfo.TYPE_STUB) {
+        if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE
+                || vol.getType() == VolumeInfo.TYPE_STUB) {
             isTypeRestricted = userManager
                     .hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
                     Binder.getCallingUserHandle());
@@ -1967,6 +1973,13 @@
         mContext = context;
         mCallbacks = new Callbacks(FgThread.get().getLooper());
 
+        mVolumes.registerObserver(new Watcher() {
+            @Override
+            public void onChange(Watchable what) {
+                // When we change the list or any volume contained in it, invalidate the cache
+                StorageManager.invalidateVolumeListCache();
+            }
+        });
         HandlerThread hthread = new HandlerThread(TAG);
         hthread.start();
         mHandler = new StorageManagerServiceHandler(hthread.getLooper());
@@ -2339,7 +2352,7 @@
 
         super.mount_enforcePermission();
 
-        final VolumeInfo vol = findVolumeByIdOrThrow(volId);
+        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
         if (isMountDisallowed(vol)) {
             throw new SecurityException("Mounting " + volId + " restricted by policy");
         }
@@ -2365,23 +2378,24 @@
         }
     }
 
-    private void mount(VolumeInfo vol) {
+    private void mount(WatchedVolumeInfo vol) {
         try {
-            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id);
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId());
             // TODO(b/135341433): Remove cautious logging when FUSE is stable
             Slog.i(TAG, "Mounting volume " + vol);
             extendWatchdogTimeout("#mount might be slow");
-            mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() {
+            mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(),
+                    new IVoldMountCallback.Stub() {
                 @Override
                 public boolean onVolumeChecking(FileDescriptor fd, String path,
                         String internalPath) {
-                    vol.path = path;
-                    vol.internalPath = internalPath;
+                    vol.setFsPath(path);
+                    vol.setInternalPath(internalPath);
                     ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd);
                     try {
                         Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
-                                "SMS.startFuseFileSystem: " + vol.id);
-                        mStorageSessionController.onVolumeMount(pfd, vol);
+                                "SMS.startFuseFileSystem: " + vol.getId());
+                        mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo());
                         return true;
                     } catch (ExternalStorageServiceException e) {
                         Slog.e(TAG, "Failed to mount volume " + vol, e);
@@ -2416,21 +2430,21 @@
 
         super.unmount_enforcePermission();
 
-        final VolumeInfo vol = findVolumeByIdOrThrow(volId);
+        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
         unmount(vol);
     }
 
-    private void unmount(VolumeInfo vol) {
+    private void unmount(WatchedVolumeInfo vol) {
         try {
             try {
-                if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+                if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
                     mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
                 }
             } catch (Installer.InstallerException e) {
                 Slog.e(TAG, "Failed unmount mirror data", e);
             }
-            mVold.unmount(vol.id);
-            mStorageSessionController.onVolumeUnmount(vol);
+            mVold.unmount(vol.getId());
+            mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
         } catch (Exception e) {
             Slog.wtf(TAG, e);
         }
@@ -2442,10 +2456,10 @@
 
         super.format_enforcePermission();
 
-        final VolumeInfo vol = findVolumeByIdOrThrow(volId);
-        final String fsUuid = vol.fsUuid;
+        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
+        final String fsUuid = vol.getFsUuid();
         try {
-            mVold.format(vol.id, "auto");
+            mVold.format(vol.getId(), "auto");
 
             // After a successful format above, we should forget about any
             // records for the old partition, since it'll never appear again
@@ -3105,7 +3119,7 @@
     private void warnOnNotMounted() {
         synchronized (mLock) {
             for (int i = 0; i < mVolumes.size(); i++) {
-                final VolumeInfo vol = mVolumes.valueAt(i);
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                 if (vol.isPrimary() && vol.isMountedWritable()) {
                     // Cool beans, we have a mounted primary volume
                     return;
@@ -3392,8 +3406,8 @@
         }
     }
 
-    private void prepareUserStorageIfNeeded(VolumeInfo vol) throws Exception {
-        if (vol.type != VolumeInfo.TYPE_PRIVATE) {
+    private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception {
+        if (vol.getType() != VolumeInfo.TYPE_PRIVATE) {
             return;
         }
 
@@ -3411,7 +3425,7 @@
                 continue;
             }
 
-            prepareUserStorageInternal(vol.fsUuid, user.id, flags);
+            prepareUserStorageInternal(vol.getFsUuid(), user.id, flags);
         }
     }
 
@@ -3960,7 +3974,7 @@
         synchronized (mLock) {
             for (int i = 0; i < mVolumes.size(); i++) {
                 final String volId = mVolumes.keyAt(i);
-                final VolumeInfo vol = mVolumes.valueAt(i);
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                 switch (vol.getType()) {
                     case VolumeInfo.TYPE_PUBLIC:
                     case VolumeInfo.TYPE_STUB:
@@ -4112,7 +4126,7 @@
         synchronized (mLock) {
             final VolumeInfo[] res = new VolumeInfo[mVolumes.size()];
             for (int i = 0; i < mVolumes.size(); i++) {
-                res[i] = mVolumes.valueAt(i);
+                res[i] = mVolumes.valueAt(i).getVolumeInfo();
             }
             return res;
         }
@@ -4708,7 +4722,8 @@
                     break;
                 }
                 case MSG_VOLUME_STATE_CHANGED: {
-                    callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
+                    VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo();
+                    callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3);
                     break;
                 }
                 case MSG_VOLUME_RECORD_CHANGED: {
@@ -4738,7 +4753,7 @@
             obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget();
         }
 
-        private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
+        private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) {
             final SomeArgs args = SomeArgs.obtain();
             args.arg1 = vol.clone();
             args.argi2 = oldState;
@@ -4790,8 +4805,8 @@
             pw.println("Volumes:");
             pw.increaseIndent();
             for (int i = 0; i < mVolumes.size(); i++) {
-                final VolumeInfo vol = mVolumes.valueAt(i);
-                if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) continue;
+                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+                if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue;
                 vol.dump(pw);
             }
             pw.decreaseIndent();
@@ -5088,7 +5103,7 @@
             final List<String> primaryVolumeIds = new ArrayList<>();
             synchronized (mLock) {
                 for (int i = 0; i < mVolumes.size(); i++) {
-                    final VolumeInfo vol = mVolumes.valueAt(i);
+                    final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                     if (vol.isPrimary()) {
                         primaryVolumeIds.add(vol.getId());
                     }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 6cca7d1..cce2959 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8302,8 +8302,6 @@
         if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) {
             @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
                     callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
-            // We store them to compare the old and new while-in-use logics to each other.
-            // (They're not used for any other purposes.)
             if (allowWiu == REASON_DENIED) {
                 allowWiu = allowWhileInUse;
             }
@@ -8706,6 +8704,7 @@
                                         + ",duration:" + tempAllowListReason.mDuration
                                         + ",callingUid:" + tempAllowListReason.mCallingUid))
                         + ">"
+                        + "; allowWiu:" + allowWhileInUse
                         + "; targetSdkVersion:" + r.appInfo.targetSdkVersion
                         + "; callerTargetSdkVersion:" + callerTargetSdkVersion
                         + "; startForegroundCount:" + r.mStartForegroundCount
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index c8b0a57..5ff6999 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -3705,8 +3705,14 @@
     @Override
     public void takeUidSnapshotsAsync(int[] requestUids, ResultReceiver resultReceiver) {
         if (!onlyCaller(requestUids)) {
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.BATTERY_STATS, null);
+            try {
+                mContext.enforceCallingOrSelfPermission(
+                        android.Manifest.permission.BATTERY_STATS, null);
+            } catch (SecurityException ex) {
+                resultReceiver.send(IBatteryStats.RESULT_SECURITY_EXCEPTION,
+                        Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage()));
+                return;
+            }
         }
 
         if (shouldCollectExternalStats()) {
@@ -3727,13 +3733,14 @@
                 }
                 Bundle resultData = new Bundle(1);
                 resultData.putParcelableArray(IBatteryStats.KEY_UID_SNAPSHOTS, results);
-                resultReceiver.send(0, resultData);
+                resultReceiver.send(IBatteryStats.RESULT_OK, resultData);
             } catch (Exception ex) {
                 if (DBG) {
                     Slog.d(TAG, "Crashed while returning results for takeUidSnapshots("
                             + Arrays.toString(requestUids) + ") i=" + i, ex);
                 }
-                throw ex;
+                resultReceiver.send(IBatteryStats.RESULT_RUNTIME_EXCEPTION,
+                        Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage()));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index c633830..f1007e7 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -186,9 +186,28 @@
         "core_libraries",
         "crumpet",
         "dck_framework",
+        "desktop_apps",
+        "desktop_better_together",
+        "desktop_bsp",
+        "desktop_camera",
         "desktop_connectivity",
+        "desktop_display",
+        "desktop_commercial",
+        "desktop_firmware",
+        "desktop_graphics",
         "desktop_hwsec",
+        "desktop_input",
+        "desktop_kernel",
+        "desktop_ml",
+        "desktop_serviceability",
+        "desktop_oobe",
+        "desktop_peripherals",
+        "desktop_pnp",
+        "desktop_security",
         "desktop_stats",
+        "desktop_sysui",
+        "desktop_users_and_accounts",
+        "desktop_video",
         "desktop_wifi",
         "devoptions_settings",
         "game",
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index e0fbaf4..27e9e44 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -31,7 +31,6 @@
 import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
 import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
 import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
 import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED;
 import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED;
 import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -3905,6 +3904,10 @@
             return mService.mWindowManager;
         }
 
+        ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+            return mService.mAtmInternal;
+        }
+
         void activityManagerOnUserStopped(@UserIdInt int userId) {
             LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId);
         }
@@ -4119,25 +4122,40 @@
         }
 
         void lockDeviceNowAndWaitForKeyguardShown() {
+            if (getWindowManager().isKeyguardLocked()) {
+                Slogf.w(TAG, "Not locking the device since the keyguard is already locked");
+                return;
+            }
+
             final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
             t.traceBegin("lockDeviceNowAndWaitForKeyguardShown");
 
             final CountDownLatch latch = new CountDownLatch(1);
-            Bundle bundle = new Bundle();
-            bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() {
-                public void sendResult(Bundle data) {
-                    latch.countDown();
-                }
-            });
-            getWindowManager().lockNow(bundle);
+            ActivityTaskManagerInternal.ScreenObserver screenObserver =
+                    new ActivityTaskManagerInternal.ScreenObserver() {
+                        @Override
+                        public void onAwakeStateChanged(boolean isAwake) {
+
+                        }
+
+                        @Override
+                        public void onKeyguardStateChanged(boolean isShowing) {
+                            if (isShowing) {
+                                latch.countDown();
+                            }
+                        }
+                    };
+
+            getActivityTaskManagerInternal().registerScreenObserver(screenObserver);
+            getWindowManager().lockDeviceNow();
             try {
                 if (!latch.await(20, TimeUnit.SECONDS)) {
-                    throw new RuntimeException("User controller expected a callback while waiting "
-                            + "to show the keyguard. Timed out after 20 seconds.");
+                    throw new RuntimeException("Keyguard is not shown in 20 seconds");
                 }
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             } finally {
+                getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
                 t.traceEnd();
             }
         }
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index c0a97db..7672011 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -361,6 +361,14 @@
                 case POPULATE_GAME_MODE_SETTINGS: {
                     removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
                     final int userId = (int) msg.obj;
+                    synchronized (mLock) {
+                        if (!mSettings.containsKey(userId)) {
+                            GameManagerSettings userSettings = new GameManagerSettings(
+                                    Environment.getDataSystemDeDirectory(userId));
+                            mSettings.put(userId, userSettings);
+                            userSettings.readPersistentDataLocked();
+                        }
+                    }
                     final String[] packageNames = getInstalledGamePackageNames(userId);
                     updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames);
                     break;
@@ -990,8 +998,7 @@
         @Override
         public void onUserStarting(@NonNull TargetUser user) {
             Slog.d(TAG, "Starting user " + user.getUserIdentifier());
-            mService.onUserStarting(user,
-                    Environment.getDataSystemDeDirectory(user.getUserIdentifier()));
+            mService.onUserStarting(user, /*settingDataDirOverride*/ null);
         }
 
         @Override
@@ -1596,13 +1603,16 @@
         }
     }
 
-    void onUserStarting(@NonNull TargetUser user, File settingDataDir) {
+    void onUserStarting(@NonNull TargetUser user, File settingDataDirOverride) {
         final int userId = user.getUserIdentifier();
-        synchronized (mLock) {
-            if (!mSettings.containsKey(userId)) {
-                GameManagerSettings userSettings = new GameManagerSettings(settingDataDir);
-                mSettings.put(userId, userSettings);
-                userSettings.readPersistentDataLocked();
+        if (settingDataDirOverride != null) {
+            synchronized (mLock) {
+                if (!mSettings.containsKey(userId)) {
+                    GameManagerSettings userSettings = new GameManagerSettings(
+                            settingDataDirOverride);
+                    mSettings.put(userId, userSettings);
+                    userSettings.readPersistentDataLocked();
+                }
             }
         }
         sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_STARTING,
diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java
index c57a1f7..fd4bf2f 100644
--- a/services/core/java/com/android/server/app/GameManagerSettings.java
+++ b/services/core/java/com/android/server/app/GameManagerSettings.java
@@ -221,9 +221,7 @@
             return false;
         }
 
-        try {
-            final FileInputStream str = mSettingsFile.openRead();
-
+        try (FileInputStream str = mSettingsFile.openRead()) {
             final TypedXmlPullParser parser = Xml.resolvePullParser(str);
             int type;
             while ((type = parser.next()) != XmlPullParser.START_TAG
@@ -251,7 +249,6 @@
                             + type);
                 }
             }
-            str.close();
         } catch (XmlPullParserException | java.io.IOException e) {
             Slog.wtf(TAG, "Error reading game manager settings", e);
             return false;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 2219ecc..b48d0a6 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -66,7 +66,6 @@
 import static com.android.media.audio.Flags.replaceStreamBtSco;
 import static com.android.media.audio.Flags.ringMyCar;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
-import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
 import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
@@ -4977,9 +4976,8 @@
                 + roForegroundAudioControl());
         pw.println("\tandroid.media.audio.scoManagedByAudio:"
                 + scoManagedByAudio());
-        pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
-                + vgsVssSyncMuteOrder());
         pw.println("\tcom.android.media.audio.absVolumeIndexFix - EOL");
+        pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder - EOL");
         pw.println("\tcom.android.media.audio.replaceStreamBtSco:"
                 + replaceStreamBtSco());
         pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:"
@@ -9010,22 +9008,13 @@
                                         synced = true;
                                         continue;
                                     }
-                                    if (vgsVssSyncMuteOrder()) {
-                                        if ((isMuted() != streamMuted) && isVssMuteBijective(
-                                                stream)) {
-                                            vss.mute(isMuted(), "VGS.applyAllVolumes#1");
-                                        }
+                                    if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+                                        vss.mute(isMuted(), "VGS.applyAllVolumes#1");
                                     }
                                     if (indexForStream != index) {
                                         vss.setIndex(index * 10, device,
                                                 caller, true /*hasModifyAudioSettings*/);
                                     }
-                                    if (!vgsVssSyncMuteOrder()) {
-                                        if ((isMuted() != streamMuted) && isVssMuteBijective(
-                                                stream)) {
-                                            vss.mute(isMuted(), "VGS.applyAllVolumes#1");
-                                        }
-                                    }
                                 }
                             }
                         }
@@ -15093,11 +15082,13 @@
         final String key = "additional_output_device_delay";
         final String reply = AudioSystem.getParameters(
                 key + "=" + device.getInternalType() + "," + device.getAddress());
-        long delayMillis;
-        try {
-            delayMillis = Long.parseLong(reply.substring(key.length() + 1));
-        } catch (NullPointerException e) {
-            delayMillis = 0;
+        long delayMillis = 0;
+        if (reply.contains(key)) {
+            try {
+                delayMillis = Long.parseLong(reply.substring(key.length() + 1));
+            } catch (NullPointerException e) {
+                delayMillis = 0;
+            }
         }
         return delayMillis;
     }
@@ -15123,11 +15114,13 @@
         final String key = "max_additional_output_device_delay";
         final String reply = AudioSystem.getParameters(
                 key + "=" + device.getInternalType() + "," + device.getAddress());
-        long delayMillis;
-        try {
-            delayMillis = Long.parseLong(reply.substring(key.length() + 1));
-        } catch (NullPointerException e) {
-            delayMillis = 0;
+        long delayMillis = 0;
+        if (reply.contains(key)) {
+            try {
+                delayMillis = Long.parseLong(reply.substring(key.length() + 1));
+            } catch (NullPointerException e) {
+                delayMillis = 0;
+            }
         }
         return delayMillis;
     }
diff --git a/services/core/java/com/android/server/backup/InputBackupHelper.java b/services/core/java/com/android/server/backup/InputBackupHelper.java
new file mode 100644
index 0000000..af9606c
--- /dev/null
+++ b/services/core/java/com/android/server/backup/InputBackupHelper.java
@@ -0,0 +1,82 @@
+/*
+ * 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.server.backup;
+
+import static com.android.server.input.InputManagerInternal.BACKUP_CATEGORY_INPUT_GESTURES;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.backup.BlobBackupHelper;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class InputBackupHelper extends BlobBackupHelper {
+    private static final String TAG = "InputBackupHelper";   // must be < 23 chars
+
+    // Current version of the blob schema
+    private static final int BLOB_VERSION = 1;
+
+    // Key under which the payload blob is stored
+    private static final String KEY_INPUT_GESTURES = "input_gestures";
+
+    private final @UserIdInt int mUserId;
+
+    private final @NonNull InputManagerInternal mInputManagerInternal;
+
+    public InputBackupHelper(int userId) {
+        super(BLOB_VERSION, KEY_INPUT_GESTURES);
+        mUserId = userId;
+        mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        Map<Integer, byte[]> payloads;
+        try {
+            payloads = mInputManagerInternal.getBackupPayload(mUserId);
+        } catch (Exception exception) {
+            Slog.e(TAG, "Failed to get backup payload for input gestures", exception);
+            return null;
+        }
+
+        if (KEY_INPUT_GESTURES.equals(key)) {
+            return payloads.getOrDefault(BACKUP_CATEGORY_INPUT_GESTURES, null);
+        }
+
+        return null;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        Map<Integer, byte[]> payloads = new HashMap<>();
+        if (KEY_INPUT_GESTURES.equals(key)) {
+            payloads.put(BACKUP_CATEGORY_INPUT_GESTURES, payload);
+        }
+
+        try {
+            mInputManagerInternal.applyBackupPayload(payloads, mUserId);
+        } catch (Exception exception) {
+            Slog.e(TAG, "Failed to apply input backup payload", exception);
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 677e0c0..b11267e 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -68,6 +68,7 @@
     private static final String COMPANION_HELPER = "companion";
     private static final String SYSTEM_GENDER_HELPER = "system_gender";
     private static final String DISPLAY_HELPER = "display";
+    private static final String INPUT_HELPER = "input";
 
     // These paths must match what the WallpaperManagerService uses.  The leaf *_FILENAME
     // are also used in the full-backup file format, so must not change unless steps are
@@ -112,7 +113,7 @@
     private static final Set<String> sEligibleHelpersForNonSystemUser =
             SetUtils.union(sEligibleHelpersForProfileUser,
                     Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER,
-                            SHORTCUT_MANAGER_HELPER));
+                            SHORTCUT_MANAGER_HELPER, INPUT_HELPER));
 
     private int mUserId = UserHandle.USER_SYSTEM;
     private boolean mIsProfileUser = false;
@@ -149,6 +150,9 @@
         addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER,
                 new SystemGrammaticalGenderBackupHelper(mUserId));
         addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId));
+        if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) {
+            addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId));
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 471b7b4..d412277 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -24,8 +24,10 @@
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.content.Context;
+import android.hardware.display.IVirtualDisplayCallback;
 import android.os.LocaleList;
 import android.util.ArraySet;
+import android.window.DisplayWindowPolicyController;
 
 import java.util.Set;
 import java.util.function.Consumer;
@@ -104,6 +106,17 @@
     public abstract @NonNull ArraySet<Integer> getDeviceIdsForUid(int uid);
 
     /**
+     * Notifies that a virtual display was created.
+     *
+     * @param virtualDevice The virtual device that owns the virtual display.
+     * @param displayId     The display id of the created virtual display.
+     * @param callback      The callback of the virtual display.
+     * @param dwpc          The DisplayWindowPolicyController of the created virtual display.
+     */
+    public abstract void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+            IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc);
+
+    /**
      * Notifies that a virtual display is removed.
      *
      * @param virtualDevice The virtual device where the virtual display located.
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e83efc5..854b0dd 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2041,6 +2041,7 @@
                                 packageName,
                                 displayUniqueId,
                                 virtualDevice,
+                                dwpc,
                                 surface,
                                 flags,
                                 virtualDisplayConfig);
@@ -2135,6 +2136,7 @@
             String packageName,
             String uniqueId,
             IVirtualDevice virtualDevice,
+            DisplayWindowPolicyController dwpc,
             Surface surface,
             int flags,
             VirtualDisplayConfig virtualDisplayConfig) {
@@ -2188,6 +2190,16 @@
 
         final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
         if (display != null) {
+            // Notify the virtual device that the display has been created. This needs to be called
+            // in this locked section before the repository had the chance to notify any listeners
+            // to ensure that the device is aware of the new display before others know about it.
+            if (virtualDevice != null) {
+                final VirtualDeviceManagerInternal vdm =
+                        getLocalService(VirtualDeviceManagerInternal.class);
+                vdm.onVirtualDisplayCreated(
+                        virtualDevice, display.getDisplayIdLocked(), callback, dwpc);
+            }
+
             return display.getDisplayIdLocked();
         }
 
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b49c01b..83ca563 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -781,6 +781,11 @@
                     if (isDisplayPrivate(physicalAddress)) {
                         mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
                     }
+
+                    if (isDisplayStealTopFocusDisabled(physicalAddress)) {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_FOCUS;
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED;
+                    }
                 }
 
                 if (DisplayCutout.getMaskBuiltInDisplayCutout(res, mInfo.uniqueId)) {
@@ -1467,6 +1472,23 @@
             }
             return false;
         }
+
+        private boolean isDisplayStealTopFocusDisabled(DisplayAddress.Physical physicalAddress) {
+            if (physicalAddress == null) {
+                return false;
+            }
+            final Resources res = getOverlayContext().getResources();
+            int[] ports = res.getIntArray(R.array.config_localNotStealTopFocusDisplayPorts);
+            if (ports != null) {
+                int port = physicalAddress.getPort();
+                for (int p : ports) {
+                    if (p == port) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
     }
 
     private boolean hdrTypesEqual(int[] modeHdrTypes, int[] recordHdrTypes) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index a1e8f08..aab2760 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -122,11 +122,6 @@
             Flags.FLAG_ALWAYS_ROTATE_DISPLAY_DEVICE,
             Flags::alwaysRotateDisplayDevice);
 
-    private final FlagState mRefreshRateVotingTelemetry = new FlagState(
-            Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY,
-            Flags::refreshRateVotingTelemetry
-    );
-
     private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState(
             Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION,
             Flags::enablePixelAnisotropyCorrection
@@ -403,10 +398,6 @@
         return mAlwaysRotateDisplayDevice.isEnabled();
     }
 
-    public boolean isRefreshRateVotingTelemetryEnabled() {
-        return mRefreshRateVotingTelemetry.isEnabled();
-    }
-
     public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() {
         return mPixelAnisotropyCorrectionEnabled.isEnabled();
     }
@@ -626,7 +617,6 @@
         pw.println(" " + mAutoBrightnessModesFlagState);
         pw.println(" " + mFastHdrTransitions);
         pw.println(" " + mAlwaysRotateDisplayDevice);
-        pw.println(" " + mRefreshRateVotingTelemetry);
         pw.println(" " + mPixelAnisotropyCorrectionEnabled);
         pw.println(" " + mSensorBasedBrightnessThrottling);
         pw.println(" " + mIdleScreenRefreshRateTimeout);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index cc0bbde..8211feb 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -191,14 +191,6 @@
 }
 
 flag {
-    name: "refresh_rate_voting_telemetry"
-    namespace: "display_manager"
-    description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager"
-    bug: "310029108"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "enable_pixel_anisotropy_correction"
     namespace: "display_manager"
     description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling"
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 1dd4a9b..c37733b 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -229,8 +229,7 @@
         mContext = context;
         mHandler = new DisplayModeDirectorHandler(handler.getLooper());
         mInjector = injector;
-        mVotesStatsReporter = injector.getVotesStatsReporter(
-                displayManagerFlags.isRefreshRateVotingTelemetryEnabled());
+        mVotesStatsReporter = injector.getVotesStatsReporter();
         mSupportedModesByDisplay = new SparseArray<>();
         mAppSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
@@ -3141,7 +3140,7 @@
         SensorManagerInternal getSensorManagerInternal();
 
         @Nullable
-        VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled);
+        VotesStatsReporter getVotesStatsReporter();
     }
 
     @VisibleForTesting
@@ -3281,10 +3280,9 @@
         }
 
         @Override
-        public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) {
+        public VotesStatsReporter getVotesStatsReporter() {
             // if frame rate override supported, renderRates will be ignored in mode selection
-            return new VotesStatsReporter(supportsFrameRateOverride(),
-                    refreshRateVotingTelemetryEnabled);
+            return new VotesStatsReporter(supportsFrameRateOverride());
         }
 
         private DisplayManager getDisplayManager() {
diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
index 7562a52..7b579c0 100644
--- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
+++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
@@ -25,6 +25,7 @@
 import android.annotation.Nullable;
 import android.os.Trace;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.view.Display;
 
 import com.android.internal.util.FrameworkStatsLog;
@@ -36,13 +37,11 @@
     private static final String TAG = "VotesStatsReporter";
     private static final int REFRESH_RATE_NOT_LIMITED = 1000;
     private final boolean mIgnoredRenderRate;
-    private final boolean mFrameworkStatsLogReportingEnabled;
 
-    private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1;
+    private final SparseIntArray mLastMinPriorityByDisplay = new SparseIntArray();
 
-    public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) {
+    VotesStatsReporter(boolean ignoreRenderRate) {
         mIgnoredRenderRate = ignoreRenderRate;
-        mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled;
     }
 
     void reportVoteChanged(int displayId, int priority,  @Nullable Vote vote) {
@@ -57,32 +56,27 @@
         int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate);
-        if (mFrameworkStatsLogReportingEnabled) {
-            FrameworkStatsLog.write(
-                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
-                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
-                    maxRefreshRate, -1);
-        }
+        FrameworkStatsLog.write(
+                DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+                maxRefreshRate, -1);
     }
 
     private void reportVoteRemoved(int displayId, int priority) {
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1);
-        if (mFrameworkStatsLogReportingEnabled) {
-            FrameworkStatsLog.write(
-                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
-                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
-        }
+        FrameworkStatsLog.write(
+                DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
     }
 
     void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode,
             SparseArray<Vote> votes) {
-        if (!mFrameworkStatsLogReportingEnabled) {
-            return;
-        }
+        int lastMinPriorityReported = mLastMinPriorityByDisplay.get(
+                displayId, Vote.MAX_PRIORITY + 1);
         int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
         for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) {
-            if (priority < mLastMinPriorityReported && priority < minPriority) {
+            if (priority < lastMinPriorityReported && priority < minPriority) {
                 continue;
             }
             Vote vote = votes.get(priority);
@@ -91,7 +85,7 @@
             }
 
             // Was previously reported ACTIVE, changed to ADDED
-            if (priority >= mLastMinPriorityReported && priority < minPriority) {
+            if (priority >= lastMinPriorityReported && priority < minPriority) {
                 int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
                 FrameworkStatsLog.write(
                         DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
@@ -99,7 +93,7 @@
                         maxRefreshRate, selectedRefreshRate);
             }
             // Was previously reported ADDED, changed to ACTIVE
-            if (priority >= minPriority && priority < mLastMinPriorityReported) {
+            if (priority >= minPriority && priority < lastMinPriorityReported) {
                 int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
                 FrameworkStatsLog.write(
                         DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
@@ -107,7 +101,7 @@
                         maxRefreshRate, selectedRefreshRate);
             }
 
-            mLastMinPriorityReported = minPriority;
+            mLastMinPriorityByDisplay.put(displayId, minPriority);
         }
     }
 
diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig
index 4505d0e..7e5c1bc 100644
--- a/services/core/java/com/android/server/flags/services.aconfig
+++ b/services/core/java/com/android/server/flags/services.aconfig
@@ -86,3 +86,10 @@
     description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings"
     bug: "283267917"
 }
+
+flag {
+    name: "certpininstaller_removal"
+    namespace: "network_security"
+    description: "Remove CertPinInstallReceiver from the platform"
+    bug: "391205997"
+}
diff --git a/services/core/java/com/android/server/input/InputDataStore.java b/services/core/java/com/android/server/input/InputDataStore.java
index e8f21fe..834f815 100644
--- a/services/core/java/com/android/server/input/InputDataStore.java
+++ b/services/core/java/com/android/server/input/InputDataStore.java
@@ -125,8 +125,20 @@
         }
     }
 
-    @VisibleForTesting
-    List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
+    /**
+     * Parses the given input stream and returns the list of {@link InputGestureData} objects.
+     * This parsing happens on a best effort basis. If invalid data exists in the given payload
+     * it will be skipped. An example of this would be a keycode that does not exist in the
+     * present version of Android.  If the payload is malformed, instead this will throw an
+     * exception and require the caller to handel this appropriately for its situation.
+     *
+     * @param stream stream of the input payload of XML data
+     * @param utf8Encoded whether or not the input data is UTF-8 encoded
+     * @return list of {@link InputGestureData} objects pulled from the payload
+     * @throws XmlPullParserException
+     * @throws IOException
+     */
+    public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
             throws XmlPullParserException, IOException {
         List<InputGestureData> inputGestureDataList = new ArrayList<>();
         TypedXmlPullParser parser;
@@ -153,6 +165,31 @@
         return inputGestureDataList;
     }
 
+    /**
+     * Serializes the given list of {@link InputGestureData} objects to XML in the provided output
+     * stream.
+     *
+     * @param stream               output stream to put serialized data.
+     * @param utf8Encoded          whether or not to encode the serialized data in UTF-8 format.
+     * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize.
+     */
+    public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
+            List<InputGestureData> inputGestureDataList) throws IOException {
+        final TypedXmlSerializer serializer;
+        if (utf8Encoded) {
+            serializer = Xml.newFastSerializer();
+            serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+        } else {
+            serializer = Xml.resolveSerializer(stream);
+        }
+
+        serializer.startDocument(null, true);
+        serializer.startTag(null, TAG_ROOT);
+        writeInputGestureListToXml(serializer, inputGestureDataList);
+        serializer.endTag(null, TAG_ROOT);
+        serializer.endDocument();
+    }
+
     private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException, IllegalArgumentException {
         InputGestureData.Builder builder = new InputGestureData.Builder();
@@ -239,24 +276,6 @@
         return inputGestureDataList;
     }
 
-    @VisibleForTesting
-    void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
-            List<InputGestureData> inputGestureDataList) throws IOException {
-        final TypedXmlSerializer serializer;
-        if (utf8Encoded) {
-            serializer = Xml.newFastSerializer();
-            serializer.setOutput(stream, StandardCharsets.UTF_8.name());
-        } else {
-            serializer = Xml.resolveSerializer(stream);
-        }
-
-        serializer.startDocument(null, true);
-        serializer.startTag(null, TAG_ROOT);
-        writeInputGestureListToXml(serializer, inputGestureDataList);
-        serializer.endTag(null, TAG_ROOT);
-        serializer.endDocument();
-    }
-
     private void writeInputGestureToXml(TypedXmlSerializer serializer,
             InputGestureData inputGestureData) throws IOException {
         serializer.startTag(null, TAG_INPUT_GESTURE);
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 108afba..977c029 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -256,13 +256,11 @@
             ));
         }
         if (keyboardA11yShortcutControl()) {
-            if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
-                systemShortcuts.add(createKeyGesture(
-                        KeyEvent.KEYCODE_3,
-                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS
-                ));
-            }
+            systemShortcuts.add(createKeyGesture(
+                    KeyEvent.KEYCODE_3,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS
+            ));
             if (InputSettings.isAccessibilityMouseKeysFeatureFlagEnabled()) {
                 systemShortcuts.add(createKeyGesture(
                         KeyEvent.KEYCODE_4,
@@ -270,20 +268,16 @@
                         KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS
                 ));
             }
-            if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
-                systemShortcuts.add(createKeyGesture(
-                        KeyEvent.KEYCODE_5,
-                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS
-                ));
-            }
-            if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
-                systemShortcuts.add(createKeyGesture(
-                        KeyEvent.KEYCODE_6,
-                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS
-                ));
-            }
+            systemShortcuts.add(createKeyGesture(
+                    KeyEvent.KEYCODE_5,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS
+            ));
+            systemShortcuts.add(createKeyGesture(
+                    KeyEvent.KEYCODE_6,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS
+            ));
         }
         synchronized (mGestureLock) {
             for (InputGestureData systemShortcut : systemShortcuts) {
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index d2486fe..87f693c 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -16,6 +16,7 @@
 
 package com.android.server.input;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -32,7 +33,11 @@
 import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.policy.IShortcutService;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Input manager local system service interface.
@@ -41,6 +46,15 @@
  */
 public abstract class InputManagerInternal {
 
+    // Backup and restore information for custom input gestures.
+    public static final int BACKUP_CATEGORY_INPUT_GESTURES = 0;
+
+    // Backup and Restore categories for sending map of data back and forth to backup and restore
+    // infrastructure.
+    @IntDef({BACKUP_CATEGORY_INPUT_GESTURES})
+    public @interface BackupCategory {
+    }
+
     /**
      * Called by the display manager to set information about the displays as needed
      * by the input system.  The input system must copy this information to retain it.
@@ -312,4 +326,22 @@
      * @return true if setting power wakeup was successful.
      */
     public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
+    /**
+     * Retrieves the input gestures backup payload data.
+     *
+     * @param userId the user ID of the backup data.
+     * @return byte array of UTF-8 encoded backup data.
+     */
+    public abstract Map<Integer, byte[]> getBackupPayload(int userId) throws IOException;
+
+    /**
+     * Applies the given UTF-8 encoded byte array payload to the given user's input data
+     * on a best effort basis.
+     *
+     * @param payload UTF-8 encoded map of byte arrays of restored data
+     * @param userId the user ID for which to apply the payload data
+     */
+    public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+            throws XmlPullParserException, IOException;
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 2ad5a15..8624f42 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -24,6 +24,7 @@
 import static android.view.KeyEvent.KEYCODE_UNKNOWN;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.keyEventActivityDetection;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
@@ -153,6 +154,8 @@
 
 import libcore.io.IoUtils;
 
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -2772,7 +2775,7 @@
                 }
                 return true;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
-                if (complete && InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
+                if (complete) {
                     final boolean bounceKeysEnabled =
                             InputSettings.isAccessibilityBounceKeysEnabled(mContext);
                     InputSettings.setAccessibilityBounceKeysThreshold(mContext,
@@ -2790,7 +2793,7 @@
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
-                if (complete && InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+                if (complete) {
                     final boolean stickyKeysEnabled =
                             InputSettings.isAccessibilityStickyKeysEnabled(mContext);
                     InputSettings.setAccessibilityStickyKeysEnabled(mContext, !stickyKeysEnabled);
@@ -2798,7 +2801,7 @@
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
-                if (complete && InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
+                if (complete) {
                     final boolean slowKeysEnabled =
                             InputSettings.isAccessibilitySlowKeysEnabled(mContext);
                     InputSettings.setAccessibilitySlowKeysThreshold(mContext,
@@ -3805,6 +3808,26 @@
         public boolean setKernelWakeEnabled(int deviceId, boolean enabled) {
             return mNative.setKernelWakeEnabled(deviceId, enabled);
         }
+
+        @Override
+        public Map<Integer, byte[]> getBackupPayload(int userId) throws IOException {
+            final Map<Integer, byte[]> payload = new HashMap<>();
+            if (enableCustomizableInputGestures()) {
+                payload.put(BACKUP_CATEGORY_INPUT_GESTURES,
+                        mKeyGestureController.getInputGestureBackupPayload(userId));
+            }
+            return payload;
+        }
+
+        @Override
+        public void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+                throws XmlPullParserException, IOException {
+            if (enableCustomizableInputGestures() && payload.containsKey(
+                    BACKUP_CATEGORY_INPUT_GESTURES)) {
+                mKeyGestureController.applyInputGesturesBackupPayload(
+                        payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId);
+            }
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 41f58ae..5770a09 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -69,6 +69,11 @@
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.KeyCombinationManager;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayDeque;
 import java.util.HashSet;
 import java.util.List;
@@ -1191,6 +1196,29 @@
         }
     }
 
+    byte[] getInputGestureBackupPayload(int userId) throws IOException {
+        final List<InputGestureData> inputGestureDataList =
+                mInputGestureManager.getCustomInputGestures(userId, null);
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        synchronized (mInputDataStore) {
+            mInputDataStore.writeInputGestureXml(byteArrayOutputStream, true, inputGestureDataList);
+        }
+        return byteArrayOutputStream.toByteArray();
+    }
+
+    void applyInputGesturesBackupPayload(byte[] payload, int userId)
+            throws XmlPullParserException, IOException {
+        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
+        List<InputGestureData> inputGestureDataList;
+        synchronized (mInputDataStore) {
+            inputGestureDataList = mInputDataStore.readInputGesturesXml(byteArrayInputStream, true);
+        }
+        for (final InputGestureData inputGestureData : inputGestureDataList) {
+            mInputGestureManager.addCustomInputGesture(userId, inputGestureData);
+        }
+        mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
+    }
+
     // A record of a registered key gesture event listener from one process.
     private class KeyGestureEventListenerRecord implements IBinder.DeathRecipient {
         public final int mPid;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index ddace17..2ea6117 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -43,7 +43,6 @@
 
 import java.util.Collection;
 import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A class that represents a broker for the endpoint registered by the client app. This class
@@ -89,8 +88,11 @@
     /** The remote callback interface for this endpoint. */
     private final IContextHubEndpointCallback mContextHubEndpointCallback;
 
-    /** True if this endpoint is registered with the service. */
-    private AtomicBoolean mIsRegistered = new AtomicBoolean(true);
+    /** True if this endpoint is registered with the service/HAL. */
+    @GuardedBy("mRegistrationLock")
+    private boolean mIsRegistered = false;
+
+    private final Object mRegistrationLock = new Object();
 
     private final Object mOpenSessionLock = new Object();
 
@@ -192,7 +194,7 @@
     public int openSession(HubEndpointInfo destination, String serviceDescriptor)
             throws RemoteException {
         super.openSession_enforcePermission();
-        if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered");
+        if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered");
         if (!hasEndpointPermissions(destination)) {
             throw new SecurityException(
                     "Insufficient permission to open a session with endpoint: " + destination);
@@ -223,7 +225,7 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void closeSession(int sessionId, int reason) throws RemoteException {
         super.closeSession_enforcePermission();
-        if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered");
+        if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered");
         if (!cleanupSessionResources(sessionId)) {
             throw new IllegalArgumentException(
                     "Unknown session ID in closeSession: id=" + sessionId);
@@ -235,19 +237,26 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void unregister() {
         super.unregister_enforcePermission();
-        mIsRegistered.set(false);
-        try {
-            mHubInterface.unregisterEndpoint(mHalEndpointInfo);
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e);
-        }
         synchronized (mOpenSessionLock) {
             // Iterate in reverse since cleanupSessionResources will remove the entry
             for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) {
                 int id = mSessionInfoMap.keyAt(i);
+                halCloseEndpointSessionNoThrow(id, Reason.ENDPOINT_GONE);
                 cleanupSessionResources(id);
             }
         }
+        synchronized (mRegistrationLock) {
+            if (!isRegistered()) {
+                Log.w(TAG, "Attempting to unregister when already unregistered");
+                return;
+            }
+            mIsRegistered = false;
+            try {
+                mHubInterface.unregisterEndpoint(mHalEndpointInfo);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e);
+            }
+        }
         mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint());
         releaseWakeLockOnExit();
     }
@@ -335,7 +344,7 @@
     /** Invoked when the underlying binder of this broker has died at the client process. */
     @Override
     public void binderDied() {
-        if (mIsRegistered.get()) {
+        if (isRegistered()) {
             unregister();
         }
     }
@@ -365,6 +374,22 @@
         }
     }
 
+    /**
+     * Registers this endpoints with the Context Hub HAL.
+     *
+     * @throws RemoteException if the registrations fails with a RemoteException
+     */
+    /* package */ void register() throws RemoteException {
+        synchronized (mRegistrationLock) {
+            if (isRegistered()) {
+                Log.w(TAG, "Attempting to register when already registered");
+            } else {
+                mHubInterface.registerEndpoint(mHalEndpointInfo);
+                mIsRegistered = true;
+            }
+        }
+    }
+
     /* package */ void attachDeathRecipient() throws RemoteException {
         if (mContextHubEndpointCallback != null) {
             mContextHubEndpointCallback.asBinder().linkToDeath(this, 0 /* flags */);
@@ -425,6 +450,24 @@
         }
     }
 
+    /* package */ void onHalRestart() {
+        synchronized (mRegistrationLock) {
+            mIsRegistered = false;
+            try {
+                register();
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e);
+            }
+        }
+        synchronized (mOpenSessionLock) {
+            for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) {
+                int id = mSessionInfoMap.keyAt(i);
+                onCloseEndpointSession(id, Reason.HUB_RESET);
+            }
+        }
+        // TODO(b/390029594): Cancel any ongoing reliable communication transactions
+    }
+
     private Optional<Byte> onEndpointSessionOpenRequestInternal(
             int sessionId, HubEndpointInfo initiator, String serviceDescriptor) {
         if (!hasEndpointPermissions(initiator)) {
@@ -465,24 +508,21 @@
             }
             remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo();
         }
-        if (!ContextHubServiceUtil.notePermissions(
-                mAppOpsManager,
-                mUid,
-                mPackageName,
-                mAttributionTag,
-                remote.getRequiredPermissions(),
-                RECEIVE_MSG_NOTE
-                        + "-0x"
-                        + Long.toHexString(remote.getIdentifier().getHub())
-                        + "-0x"
-                        + Long.toHexString(remote.getIdentifier().getEndpoint()))) {
-            Log.e(
-                    TAG,
-                    "Dropping message from "
-                            + remote
-                            + ". "
-                            + mPackageName
-                            + " doesn't have permission");
+
+        try {
+            Binder.withCleanCallingIdentity(
+                    () -> {
+                        if (!notePermissions(remote)) {
+                            throw new RuntimeException(
+                                    "Dropping message from "
+                                            + remote
+                                            + ". "
+                                            + mPackageName
+                                            + " doesn't have permission");
+                        }
+                    });
+        } catch (RuntimeException e) {
+            Log.e(TAG, e.getMessage());
             return ErrorCode.PERMISSION_DENIED;
         }
 
@@ -553,7 +593,7 @@
     private void acquireWakeLock() {
         Binder.withCleanCallingIdentity(
                 () -> {
-                    if (mIsRegistered.get()) {
+                    if (isRegistered()) {
                         mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
                     }
                 });
@@ -608,4 +648,30 @@
         }
         return true;
     }
+
+    private boolean isRegistered() {
+        synchronized (mRegistrationLock) {
+            return mIsRegistered;
+        }
+    }
+
+    /**
+     * Utility to call notePermissions for e.g. when processing a message from a given endpoint for
+     * this broker.
+     *
+     * @param endpoint The endpoint to check permissions for this broker.
+     */
+    private boolean notePermissions(HubEndpointInfo endpoint) {
+        return ContextHubServiceUtil.notePermissions(
+                mAppOpsManager,
+                mUid,
+                mPackageName,
+                mAttributionTag,
+                endpoint.getRequiredPermissions(),
+                RECEIVE_MSG_NOTE
+                        + "-0x"
+                        + Long.toHexString(endpoint.getIdentifier().getHub())
+                        + "-0x"
+                        + Long.toHexString(endpoint.getIdentifier().getEndpoint()));
+    }
 }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index ed98bf9..06aeb62 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -206,12 +206,6 @@
         EndpointInfo halEndpointInfo =
                 ContextHubServiceUtil.createHalEndpointInfo(
                         pendingEndpointInfo, endpointId, SERVICE_HUB_ID);
-        try {
-            mHubInterface.registerEndpoint(halEndpointInfo);
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e);
-            throw e;
-        }
         broker =
                 new ContextHubEndpointBroker(
                         mContext,
@@ -222,6 +216,7 @@
                         packageName,
                         attributionTag,
                         mTransactionManager);
+        broker.register();
         mEndpointMap.put(endpointId, broker);
 
         try {
@@ -282,6 +277,14 @@
         mEndpointMap.remove(endpointId);
     }
 
+    /** Invoked by the service when the Context Hub HAL restarts. */
+    /* package */ void onHalRestart() {
+        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
+            // The broker will close existing sessions and re-register itself
+            broker.onHalRestart();
+        }
+    }
+
     @Override
     public void onEndpointSessionOpenRequest(
             int sessionId,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubHalEndpointCallback.java b/services/core/java/com/android/server/location/contexthub/ContextHubHalEndpointCallback.java
index 88764b6..a3d9429 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubHalEndpointCallback.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubHalEndpointCallback.java
@@ -21,9 +21,6 @@
 import android.hardware.contexthub.IEndpointCallback;
 import android.hardware.contexthub.Message;
 import android.hardware.contexthub.MessageDeliveryStatus;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
 import android.os.RemoteException;
 
 /** IEndpointCallback implementation. */
@@ -32,11 +29,6 @@
     private final IEndpointLifecycleCallback mEndpointLifecycleCallback;
     private final IEndpointSessionCallback mEndpointSessionCallback;
 
-    // Use this thread in case where the execution requires to be on an async service thread.
-    private final HandlerThread mHandlerThread =
-            new HandlerThread("Context Hub endpoint callback", Process.THREAD_PRIORITY_BACKGROUND);
-    private Handler mHandler;
-
     /** Interface for listening for endpoint start and stop events. */
     public interface IEndpointLifecycleCallback {
         /** Called when a batch of endpoints started. */
@@ -73,9 +65,6 @@
             IEndpointSessionCallback endpointSessionCallback) {
         mEndpointLifecycleCallback = endpointLifecycleCallback;
         mEndpointSessionCallback = endpointSessionCallback;
-
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
     }
 
     @Override
@@ -88,7 +77,7 @@
         for (int i = 0; i < halEndpointInfos.length; i++) {
             endpointInfos[i] = new HubEndpointInfo(halEndpointInfos[i]);
         }
-        mHandler.post(() -> mEndpointLifecycleCallback.onEndpointStarted(endpointInfos));
+        mEndpointLifecycleCallback.onEndpointStarted(endpointInfos);
     }
 
     @Override
@@ -98,7 +87,7 @@
         for (int i = 0; i < halEndpointIds.length; i++) {
             endpointIds[i] = new HubEndpointInfo.HubEndpointIdentifier(halEndpointIds[i]);
         }
-        mHandler.post(() -> mEndpointLifecycleCallback.onEndpointStopped(endpointIds, reason));
+        mEndpointLifecycleCallback.onEndpointStopped(endpointIds, reason);
     }
 
     @Override
@@ -109,37 +98,33 @@
                 new HubEndpointInfo.HubEndpointIdentifier(destination.hubId, destination.id);
         HubEndpointInfo.HubEndpointIdentifier initiatorId =
                 new HubEndpointInfo.HubEndpointIdentifier(initiator.hubId, initiator.id);
-        mHandler.post(
-                () ->
-                        mEndpointSessionCallback.onEndpointSessionOpenRequest(
-                                sessionId, destinationId, initiatorId, serviceDescriptor));
+        mEndpointSessionCallback.onEndpointSessionOpenRequest(
+                sessionId, destinationId, initiatorId, serviceDescriptor);
     }
 
     @Override
     public void onCloseEndpointSession(int sessionId, byte reason) throws RemoteException {
-        mHandler.post(() -> mEndpointSessionCallback.onCloseEndpointSession(sessionId, reason));
+        mEndpointSessionCallback.onCloseEndpointSession(sessionId, reason);
     }
 
     @Override
     public void onEndpointSessionOpenComplete(int sessionId) throws RemoteException {
-        mHandler.post(() -> mEndpointSessionCallback.onEndpointSessionOpenComplete(sessionId));
+        mEndpointSessionCallback.onEndpointSessionOpenComplete(sessionId);
     }
 
     @Override
     public void onMessageReceived(int sessionId, Message message) throws RemoteException {
         HubMessage hubMessage = ContextHubServiceUtil.createHubMessage(message);
-        mHandler.post(() -> mEndpointSessionCallback.onMessageReceived(sessionId, hubMessage));
+        mEndpointSessionCallback.onMessageReceived(sessionId, hubMessage);
     }
 
     @Override
     public void onMessageDeliveryStatusReceived(
             int sessionId, MessageDeliveryStatus messageDeliveryStatus) throws RemoteException {
-        mHandler.post(
-                () ->
-                        mEndpointSessionCallback.onMessageDeliveryStatusReceived(
-                                sessionId,
-                                messageDeliveryStatus.messageSequenceNumber,
-                                messageDeliveryStatus.errorCode));
+        mEndpointSessionCallback.onMessageDeliveryStatusReceived(
+                sessionId,
+                messageDeliveryStatus.messageSequenceNumber,
+                messageDeliveryStatus.errorCode);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 2b0ca14..502a7ae 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -259,6 +259,9 @@
             if (mHubInfoRegistry != null) {
                 mHubInfoRegistry.onHalRestart();
             }
+            if (mEndpointManager != null) {
+                mEndpointManager.onHalRestart();
+            }
             resetSettings();
             if (Flags.reconnectHostEndpointsAfterHalRestart()) {
                 mClientManager.forEachClientOfHub(mContextHubId,
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 12495bb..d7d0eb4 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -612,25 +612,23 @@
         networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType));
         networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
 
-        if (com.android.internal.telephony.flags.Flags.satelliteInternet()) {
-            // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
-            TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
-            if (telephonyManager != null) {
-                ServiceState state = telephonyManager.getServiceState();
-                if (state != null && state.isUsingNonTerrestrialNetwork()) {
-                    networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
-                    try {
-                        networkRequestBuilder.addTransportType(NetworkCapabilities
-                                .TRANSPORT_SATELLITE);
-                        networkRequestBuilder.removeCapability(NetworkCapabilities
-                                .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
-                    } catch (IllegalArgumentException ignored) {
-                        // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
-                        // are not recognized, meaning an old connectivity module runs on new
-                        // android in which case no network with such capabilities will be brought
-                        // up, so it's safe to ignore the exception.
-                        // TODO: Can remove the try-catch in next quarter release.
-                    }
+        // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        if (telephonyManager != null) {
+            ServiceState state = telephonyManager.getServiceState();
+            if (state != null && state.isUsingNonTerrestrialNetwork()) {
+                networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+                try {
+                    networkRequestBuilder.addTransportType(NetworkCapabilities
+                            .TRANSPORT_SATELLITE);
+                    networkRequestBuilder.removeCapability(NetworkCapabilities
+                            .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+                } catch (IllegalArgumentException ignored) {
+                    // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
+                    // are not recognized, meaning an old connectivity module runs on new
+                    // android in which case no network with such capabilities will be brought
+                    // up, so it's safe to ignore the exception.
+                    // TODO: Can remove the try-catch in next quarter release.
                 }
             }
         }
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
index b529853..058bbc0 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
@@ -267,6 +267,50 @@
         notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
     }
 
+    @Override
+    public void selectRoute(long requestId, String sessionId, String routeId) {
+        if (SYSTEM_SESSION_ID.equals(sessionId)) {
+            super.selectRoute(requestId, sessionId, routeId);
+            return;
+        }
+        synchronized (mLock) {
+            var sessionRecord = getSessionRecordByOriginalId(sessionId);
+            var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null;
+            if (proxyRecord != null) {
+                var targetSourceRouteId =
+                        proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId);
+                if (targetSourceRouteId != null) {
+                    proxyRecord.mProxy.selectRoute(
+                            requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId);
+                }
+                return;
+            }
+        }
+        notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+    }
+
+    @Override
+    public void deselectRoute(long requestId, String sessionId, String routeId) {
+        if (SYSTEM_SESSION_ID.equals(sessionId)) {
+            super.selectRoute(requestId, sessionId, routeId);
+            return;
+        }
+        synchronized (mLock) {
+            var sessionRecord = getSessionRecordByOriginalId(sessionId);
+            var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null;
+            if (proxyRecord != null) {
+                var targetSourceRouteId =
+                        proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId);
+                if (targetSourceRouteId != null) {
+                    proxyRecord.mProxy.deselectRoute(
+                            requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId);
+                }
+                return;
+            }
+        }
+        notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+    }
+
     @GuardedBy("mLock")
     private SystemMediaSessionRecord getSessionRecordByOriginalId(String sessionOriginalId) {
         if (FORCE_GLOBAL_ROUTING_SESSION) {
diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
index e2889fa..18bccd8 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
@@ -91,7 +91,7 @@
                 updateAttributes = true;
             }
             if (restrictAudioAttributesAlarm()
-                    && record.getNotification().category != CATEGORY_ALARM
+                    && !CATEGORY_ALARM.equals(record.getNotification().category)
                     && attributes.getUsage() == AudioAttributes.USAGE_ALARM) {
                 updateAttributes = true;
             }
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index cc4c2b5..068d68d 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -40,6 +40,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.ApplicationPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.SigningDetails;
@@ -173,6 +174,10 @@
      * Report a change to observers.
      */
     private void onChanged() {
+        // App visibility may have changed, which means that earlier fetches from these caches may
+        // be invalid.
+        PackageManager.invalidatePackageInfoCache();
+        ApplicationPackageManager.invalidateGetPackagesForUidCache();
         dispatchChange(this);
     }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 0aaa0fe..76c5240 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3733,8 +3733,7 @@
                 }
                 break;
             case KeyEvent.KEYCODE_3:
-                if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()
-                        && keyboardA11yShortcutControl()) {
+                if (keyboardA11yShortcutControl()) {
                     if (firstDown && event.isMetaPressed()
                             && event.isAltPressed()) {
                         final boolean bounceKeysEnabled =
@@ -3765,8 +3764,7 @@
                 }
                 break;
             case KeyEvent.KEYCODE_5:
-                if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()
-                        && keyboardA11yShortcutControl()) {
+                if (keyboardA11yShortcutControl()) {
                     if (firstDown && event.isMetaPressed() && event.isAltPressed()) {
                         final boolean stickyKeysEnabled =
                                 InputSettings.isAccessibilityStickyKeysEnabled(
@@ -3780,8 +3778,7 @@
                 }
                 break;
             case KeyEvent.KEYCODE_6:
-                if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()
-                        && keyboardA11yShortcutControl()) {
+                if (keyboardA11yShortcutControl()) {
                     if (firstDown && event.isMetaPressed() && event.isAltPressed()) {
                         final boolean slowKeysEnabled =
                                 InputSettings.isAccessibilitySlowKeysEnabled(mContext);
diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
index dc1f936..f060e4d 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
@@ -17,8 +17,8 @@
 package com.android.server.security;
 
 import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_BOOT_STATE;
-import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS;
 import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS;
+import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS;
 import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS;
 import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF;
 import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN;
@@ -47,12 +47,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.security.AttestationVerificationManagerService.DumpLogger;
 
-import org.json.JSONObject;
-
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.cert.CertPath;
 import java.security.cert.CertPathValidator;
@@ -60,7 +56,6 @@
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
-import java.security.cert.PKIXCertPathChecker;
 import java.security.cert.PKIXParameters;
 import java.security.cert.TrustAnchor;
 import java.security.cert.X509Certificate;
@@ -69,7 +64,6 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -126,6 +120,7 @@
     private final LocalDate mTestLocalPatchDate;
     private final CertificateFactory mCertificateFactory;
     private final CertPathValidator mCertPathValidator;
+    private final CertificateRevocationStatusManager mCertificateRevocationStatusManager;
     private final DumpLogger mDumpLogger;
 
     AttestationVerificationPeerDeviceVerifier(@NonNull Context context,
@@ -135,6 +130,7 @@
         mCertificateFactory = CertificateFactory.getInstance("X.509");
         mCertPathValidator = CertPathValidator.getInstance("PKIX");
         mTrustAnchors = getTrustAnchors();
+        mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext);
         mRevocationEnabled = true;
         mTestSystemDate = null;
         mTestLocalPatchDate = null;
@@ -150,6 +146,7 @@
         mCertificateFactory = CertificateFactory.getInstance("X.509");
         mCertPathValidator = CertPathValidator.getInstance("PKIX");
         mTrustAnchors = trustAnchors;
+        mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext);
         mRevocationEnabled = revocationEnabled;
         mTestSystemDate = systemDate;
         mTestLocalPatchDate = localPatchDate;
@@ -300,15 +297,14 @@
 
         CertPath certificatePath = mCertificateFactory.generateCertPath(certificates);
         PKIXParameters validationParams = new PKIXParameters(mTrustAnchors);
-        if (mRevocationEnabled) {
-            // Checks Revocation Status List based on
-            // https://developer.android.com/training/articles/security-key-attestation#certificate_status
-            PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker();
-            validationParams.addCertPathChecker(checker);
-        }
         // Do not use built-in revocation status checker.
         validationParams.setRevocationEnabled(false);
         mCertPathValidator.validate(certificatePath, validationParams);
+        if (mRevocationEnabled) {
+            // Checks Revocation Status List based on
+            // https://developer.android.com/training/articles/security-key-attestation#certificate_status
+            mCertificateRevocationStatusManager.checkRevocationStatus(certificates);
+        }
     }
 
     private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException {
@@ -574,96 +570,6 @@
                 <= maxPatchLevelDiffMonths;
     }
 
-    /**
-     * Checks certificate revocation status.
-     *
-     * Queries status list from android.googleapis.com/attestation/status and checks for
-     * the existence of certificate's serial number. If serial number exists in map, then fail.
-     */
-    private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker {
-        private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
-        private static final String STATUS_PROPERTY_KEY = "status";
-        private static final String REASON_PROPERTY_KEY = "reason";
-        private String mStatusUrl;
-        private JSONObject mJsonStatusMap;
-
-        @Override
-        public void init(boolean forward) throws CertPathValidatorException {
-            mStatusUrl = getRevocationListUrl();
-            if (mStatusUrl == null || mStatusUrl.isEmpty()) {
-                throw new CertPathValidatorException(
-                        "R.string.vendor_required_attestation_revocation_list_url is empty.");
-            }
-            // TODO(b/221067843): Update to only pull status map on non critical path and if
-            // out of date (24hrs).
-            mJsonStatusMap = getStatusMap(mStatusUrl);
-        }
-
-        @Override
-        public boolean isForwardCheckingSupported() {
-            return false;
-        }
-
-        @Override
-        public Set<String> getSupportedExtensions() {
-            return null;
-        }
-
-        @Override
-        public void check(Certificate cert, Collection<String> unresolvedCritExts)
-                throws CertPathValidatorException {
-            X509Certificate x509Certificate = (X509Certificate) cert;
-            // The json key is the certificate's serial number converted to lowercase hex.
-            String serialNumber = x509Certificate.getSerialNumber().toString(16);
-
-            if (serialNumber == null) {
-                throw new CertPathValidatorException("Certificate serial number can not be null.");
-            }
-
-            if (mJsonStatusMap.has(serialNumber)) {
-                JSONObject revocationStatus;
-                String status;
-                String reason;
-                try {
-                    revocationStatus = mJsonStatusMap.getJSONObject(serialNumber);
-                    status = revocationStatus.getString(STATUS_PROPERTY_KEY);
-                    reason = revocationStatus.getString(REASON_PROPERTY_KEY);
-                } catch (Throwable t) {
-                    throw new CertPathValidatorException("Unable get properties for certificate "
-                            + "with serial number " + serialNumber);
-                }
-                throw new CertPathValidatorException(
-                        "Invalid certificate with serial number " + serialNumber
-                                + " has status " + status
-                                + " because reason " + reason);
-            }
-        }
-
-        private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException {
-            URL url;
-            try {
-                url = new URL(stringUrl);
-            } catch (Throwable t) {
-                throw new CertPathValidatorException(
-                        "Unable to get revocation status from " + mStatusUrl, t);
-            }
-
-            try (InputStream inputStream = url.openStream()) {
-                JSONObject statusListJson = new JSONObject(
-                        new String(inputStream.readAllBytes(), UTF_8));
-                return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
-            } catch (Throwable t) {
-                throw new CertPathValidatorException(
-                        "Unable to parse revocation status from " + mStatusUrl, t);
-            }
-        }
-
-        private String getRevocationListUrl() {
-            return mContext.getResources().getString(
-                    R.string.vendor_required_attestation_revocation_list_url);
-        }
-    }
-
     /* Mutable data class for tracking dump data from verifications. */
     private static class MyDumpData extends AttestationVerificationManagerService.DumpData {
 
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
new file mode 100644
index 0000000..d36d9f5
--- /dev/null
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.security;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Environment;
+import android.os.PersistableBundle;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.X509Certificate;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Manages the revocation status of certificates used in remote attestation. */
+class CertificateRevocationStatusManager {
+    private static final String TAG = "AVF_CRL";
+    // Must be unique within system server
+    private static final int JOB_ID = 1737671340;
+    private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt";
+    private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ",";
+
+    /**
+     * The number of days since last update for which a stored revocation status can be accepted.
+     */
+    @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30;
+
+    /**
+     * The number of days since issue date for an intermediary certificate to be considered fresh
+     * and not require a revocation list check.
+     */
+    private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70;
+
+    /**
+     * The expected number of days between a certificate's issue date and notBefore date. Used to
+     * infer a certificate's issue date from its notBefore date.
+     */
+    private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2;
+
+    private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
+    private static final Object sFileLock = new Object();
+
+    private final Context mContext;
+    private final String mTestRemoteRevocationListUrl;
+    private final File mTestRevocationStatusFile;
+    private final boolean mShouldScheduleJob;
+
+    CertificateRevocationStatusManager(Context context) {
+        this(context, null, null, true);
+    }
+
+    @VisibleForTesting
+    CertificateRevocationStatusManager(
+            Context context,
+            String testRemoteRevocationListUrl,
+            File testRevocationStatusFile,
+            boolean shouldScheduleJob) {
+        mContext = context;
+        mTestRemoteRevocationListUrl = testRemoteRevocationListUrl;
+        mTestRevocationStatusFile = testRevocationStatusFile;
+        mShouldScheduleJob = shouldScheduleJob;
+    }
+
+    /**
+     * Check the revocation status of the provided {@link X509Certificate}s.
+     *
+     * <p>The provided certificates should have been validated and ordered from leaf to a
+     * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link
+     * java.security.cert.CertPath}.
+     *
+     * @param certificates List of certificates to be checked
+     * @throws CertPathValidatorException if the check failed
+     */
+    void checkRevocationStatus(List<X509Certificate> certificates)
+            throws CertPathValidatorException {
+        if (!needToCheckRevocationStatus(certificates)) {
+            return;
+        }
+        List<String> serialNumbers = new ArrayList<>();
+        for (X509Certificate certificate : certificates) {
+            String serialNumber = certificate.getSerialNumber().toString(16);
+            if (serialNumber == null) {
+                throw new CertPathValidatorException("Certificate serial number cannot be null.");
+            }
+            serialNumbers.add(serialNumber);
+        }
+        try {
+            JSONObject revocationList = fetchRemoteRevocationList();
+            Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+            for (String serialNumber : serialNumbers) {
+                areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
+            }
+            updateLastRevocationCheckData(areCertificatesRevoked);
+            for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
+                if (entry.getValue()) {
+                    throw new CertPathValidatorException(
+                            "Certificate " + entry.getKey() + " has been revoked.");
+                }
+            }
+        } catch (IOException | JSONException ex) {
+            Slog.d(TAG, "Fallback to check stored revocation status", ex);
+            if (ex instanceof IOException && mShouldScheduleJob) {
+                scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers);
+            }
+            for (X509Certificate certificate : certificates) {
+                // Assume recently issued certificates are not revoked.
+                if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) {
+                    String serialNumber = certificate.getSerialNumber().toString(16);
+                    serialNumbers.remove(serialNumber);
+                }
+            }
+            Map<String, LocalDateTime> lastRevocationCheckData;
+            try {
+                lastRevocationCheckData = getLastRevocationCheckData();
+            } catch (IOException ex2) {
+                throw new CertPathValidatorException(
+                        "Unable to load stored revocation status", ex2);
+            }
+            for (String serialNumber : serialNumbers) {
+                if (!lastRevocationCheckData.containsKey(serialNumber)
+                        || lastRevocationCheckData
+                                .get(serialNumber)
+                                .isBefore(
+                                        LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) {
+                    throw new CertPathValidatorException(
+                            "Unable to verify the revocation status of certificate "
+                                    + serialNumber);
+                }
+            }
+        }
+    }
+
+    private static boolean needToCheckRevocationStatus(
+            List<X509Certificate> certificatesOrderedLeafFirst) {
+        if (certificatesOrderedLeafFirst.isEmpty()) {
+            return false;
+        }
+        // A certificate isn't revoked when it is first issued, so we treat it as checked on its
+        // issue date.
+        if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) {
+            return true;
+        }
+        for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) {
+            if (!isIssuedWithinDays(
+                    certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isIssuedWithinDays(X509Certificate certificate, int days) {
+        LocalDate notBeforeDate =
+                LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
+        LocalDate expectedIssueData =
+                notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES);
+        return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData);
+    }
+
+    void updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+            JSONObject revocationList, Collection<String> otherCertificatesToCheck) {
+        Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck);
+        try {
+            allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet());
+        } catch (IOException ex) {
+            Slog.e(TAG, "Unable to update last check date of stored data.", ex);
+        }
+        Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+        for (String serialNumber : allCertificatesToCheck) {
+            areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
+        }
+        updateLastRevocationCheckData(areCertificatesRevoked);
+    }
+
+    /**
+     * Update the last revocation check data stored on this device.
+     *
+     * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are
+     *     whether that certificate has been revoked
+     */
+    void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) {
+        LocalDateTime now = LocalDateTime.now();
+        synchronized (sFileLock) {
+            Map<String, LocalDateTime> lastRevocationCheckData;
+            try {
+                lastRevocationCheckData = getLastRevocationCheckData();
+            } catch (IOException ex) {
+                Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex);
+                return;
+            }
+            for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
+                if (entry.getValue()) {
+                    lastRevocationCheckData.remove(entry.getKey());
+                } else {
+                    lastRevocationCheckData.put(entry.getKey(), now);
+                }
+            }
+            storeLastRevocationCheckData(lastRevocationCheckData);
+        }
+    }
+
+    Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException {
+        Map<String, LocalDateTime> data = new HashMap<>();
+        File dataFile = getLastRevocationCheckDataFile();
+        synchronized (sFileLock) {
+            if (!dataFile.exists()) {
+                return data;
+            }
+            String dataString;
+            try (FileInputStream in = new FileInputStream(dataFile)) {
+                dataString = new String(in.readAllBytes(), UTF_8);
+            }
+            for (String line : dataString.split(System.lineSeparator())) {
+                String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER);
+                if (elements.length != 2) {
+                    continue;
+                }
+                try {
+                    data.put(elements[0], LocalDateTime.parse(elements[1]));
+                } catch (DateTimeParseException ex) {
+                    Slog.e(
+                            TAG,
+                            "Unable to parse last checked LocalDateTime from file. Deleting the"
+                                    + " potentially corrupted file.",
+                            ex);
+                    dataFile.delete();
+                    return data;
+                }
+            }
+        }
+        return data;
+    }
+
+    @VisibleForTesting
+    void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) {
+        StringBuilder dataStringBuilder = new StringBuilder();
+        for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) {
+            dataStringBuilder
+                    .append(entry.getKey())
+                    .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER)
+                    .append(entry.getValue())
+                    .append(System.lineSeparator());
+        }
+        synchronized (sFileLock) {
+            try (FileOutputStream fileOutputStream =
+                    new FileOutputStream(getLastRevocationCheckDataFile())) {
+                fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8));
+                Slog.d(TAG, "Successfully stored revocation status data.");
+            } catch (IOException ex) {
+                Slog.e(TAG, "Failed to store revocation status data.", ex);
+            }
+        }
+    }
+
+    private File getLastRevocationCheckDataFile() {
+        if (mTestRevocationStatusFile != null) {
+            return mTestRevocationStatusFile;
+        }
+        return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME);
+    }
+
+    private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) {
+        JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
+        if (jobScheduler == null) {
+            Slog.e(TAG, "Unable to get job scheduler.");
+            return;
+        }
+        Slog.d(TAG, "Scheduling job to fetch remote CRL.");
+        PersistableBundle extras = new PersistableBundle();
+        extras.putStringArray(
+                UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK,
+                serialNumbers.toArray(new String[0]));
+        jobScheduler.schedule(
+                new JobInfo.Builder(
+                                JOB_ID,
+                                new ComponentName(
+                                        mContext,
+                                        UpdateCertificateRevocationStatusJobService.class))
+                        .setExtras(extras)
+                        .setRequiredNetwork(
+                                new NetworkRequest.Builder()
+                                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                                        .build())
+                        .build());
+    }
+
+    /**
+     * Fetches the revocation list from the URL specified in
+     * R.string.vendor_required_attestation_revocation_list_url
+     *
+     * @return The remote revocation list entries in a JSONObject
+     * @throws CertPathValidatorException if the URL is not defined or is malformed.
+     * @throws IOException if the URL is valid but the fetch failed.
+     * @throws JSONException if the revocation list content cannot be parsed
+     */
+    JSONObject fetchRemoteRevocationList()
+            throws CertPathValidatorException, IOException, JSONException {
+        String urlString = getRemoteRevocationListUrl();
+        if (urlString == null || urlString.isEmpty()) {
+            throw new CertPathValidatorException(
+                    "R.string.vendor_required_attestation_revocation_list_url is empty.");
+        }
+        URL url;
+        try {
+            url = new URL(urlString);
+        } catch (MalformedURLException ex) {
+            throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex);
+        }
+        byte[] revocationListBytes;
+        try (InputStream inputStream = url.openStream()) {
+            revocationListBytes = inputStream.readAllBytes();
+        }
+        JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8));
+        return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
+    }
+
+    private String getRemoteRevocationListUrl() {
+        if (mTestRemoteRevocationListUrl != null) {
+            return mTestRemoteRevocationListUrl;
+        }
+        return mContext.getResources()
+                .getString(R.string.vendor_required_attestation_revocation_list_url);
+    }
+}
diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS
index fa4bf22..7a31a00 100644
--- a/services/core/java/com/android/server/security/OWNERS
+++ b/services/core/java/com/android/server/security/OWNERS
@@ -3,5 +3,6 @@
 include /core/java/android/security/OWNERS
 
 per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS
+per-file *CertificateRevocationStatus* = file:/core/java/android/security/attestationverification/OWNERS
 per-file FileIntegrity*.java = victorhsieh@google.com
 per-file KeyChainSystemService.java = file:platform/packages/apps/KeyChain:/OWNERS
diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
new file mode 100644
index 0000000..768c812
--- /dev/null
+++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.util.Slog;
+
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** A {@link JobService} that fetches the certificate revocation list from a remote location. */
+public class UpdateCertificateRevocationStatusJobService extends JobService {
+
+    static final String EXTRA_KEY_CERTIFICATES_TO_CHECK =
+            "com.android.server.security.extra.CERTIFICATES_TO_CHECK";
+    private static final String TAG = "AVF_CRL";
+    private ExecutorService mExecutorService;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        mExecutorService.execute(
+                () -> {
+                    try {
+                        CertificateRevocationStatusManager certificateRevocationStatusManager =
+                                new CertificateRevocationStatusManager(this);
+                        Slog.d(TAG, "Starting to fetch remote CRL from job service.");
+                        JSONObject revocationList =
+                                certificateRevocationStatusManager.fetchRemoteRevocationList();
+                        String[] certificatesToCheckFromJobParams =
+                                params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK);
+                        if (certificatesToCheckFromJobParams == null) {
+                            Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK);
+                            return;
+                        }
+                        certificateRevocationStatusManager
+                                .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+                                        revocationList,
+                                        Arrays.asList(certificatesToCheckFromJobParams));
+                    } catch (Throwable t) {
+                        Slog.e(TAG, "Unable to update the stored revocation status.", t);
+                    }
+                    jobFinished(params, false);
+                });
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mExecutorService.shutdown();
+    }
+}
diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java
new file mode 100644
index 0000000..9d60a57
--- /dev/null
+++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.storage;
+
+import android.content.Context;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.File;
+
+/**
+ * An immutable version of {@link VolumeInfo} with only getters.
+ *
+ * @hide
+ */
+public final class ImmutableVolumeInfo {
+    private final VolumeInfo mVolumeInfo;
+
+    private ImmutableVolumeInfo(VolumeInfo volumeInfo) {
+        mVolumeInfo = new VolumeInfo(volumeInfo);
+    }
+
+    public static ImmutableVolumeInfo fromVolumeInfo(VolumeInfo info) {
+        return new ImmutableVolumeInfo(info);
+    }
+
+    public ImmutableVolumeInfo clone() {
+        return fromVolumeInfo(mVolumeInfo.clone());
+    }
+
+    public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
+        return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        mVolumeInfo.dump(pw);
+    }
+
+    public DiskInfo getDisk() {
+        return mVolumeInfo.getDisk();
+    }
+
+    public String getDiskId() {
+        return mVolumeInfo.getDiskId();
+    }
+
+    public String getFsLabel() {
+        return mVolumeInfo.fsLabel;
+    }
+
+    public String getFsPath() {
+        return mVolumeInfo.path;
+    }
+
+    public String getFsType() {
+        return mVolumeInfo.fsType;
+    }
+
+    public String getFsUuid() {
+        return mVolumeInfo.fsUuid;
+    }
+
+    public String getId() {
+        return mVolumeInfo.id;
+    }
+
+    public File getInternalPath() {
+        return mVolumeInfo.getInternalPath();
+    }
+
+    public int getMountFlags() {
+        return mVolumeInfo.mountFlags;
+    }
+
+    public int getMountUserId() {
+        return mVolumeInfo.mountUserId;
+    }
+
+    public String getPartGuid() {
+        return mVolumeInfo.partGuid;
+    }
+
+    public File getPath() {
+        return mVolumeInfo.getPath();
+    }
+
+    public int getState() {
+        return mVolumeInfo.state;
+    }
+
+    public int getType() {
+        return mVolumeInfo.type;
+    }
+
+    public VolumeInfo getVolumeInfo() {
+        return new VolumeInfo(mVolumeInfo); // Return a copy, not the original
+    }
+
+    public boolean isMountedReadable() {
+        return mVolumeInfo.isMountedReadable();
+    }
+
+    public boolean isMountedWritable() {
+        return mVolumeInfo.isMountedWritable();
+    }
+
+    public boolean isPrimary() {
+        return mVolumeInfo.isPrimary();
+    }
+
+    public boolean isVisible() {
+        return mVolumeInfo.isVisible();
+    }
+
+    public boolean isVisibleForUser(int userId) {
+        return mVolumeInfo.isVisibleForUser(userId);
+    }
+
+    public boolean isVisibleForWrite(int userId) {
+        return mVolumeInfo.isVisibleForWrite(userId);
+    }
+}
diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java
index b9c9b64..342b864 100644
--- a/services/core/java/com/android/server/storage/StorageSessionController.java
+++ b/services/core/java/com/android/server/storage/StorageSessionController.java
@@ -45,6 +45,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.storage.ImmutableVolumeInfo;
 
 import java.util.Objects;
 
@@ -79,18 +80,18 @@
      * @param vol for which the storage session has to be started
      * @return userId for connection for this volume
      */
-    public int getConnectionUserIdForVolume(VolumeInfo vol) {
+    public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) {
         final Context volumeUserContext = mContext.createContextAsUser(
-                UserHandle.of(vol.mountUserId), 0);
+                UserHandle.of(vol.getMountUserId()), 0);
         boolean isMediaSharedWithParent = volumeUserContext.getSystemService(
                 UserManager.class).isMediaSharedWithParent();
 
-        UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId);
+        UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId());
         if (userInfo != null && isMediaSharedWithParent) {
             // Clones use the same connection as their parent
             return userInfo.profileGroupId;
         } else {
-            return vol.mountUserId;
+            return vol.getMountUserId();
         }
     }
 
@@ -108,7 +109,7 @@
      * @throws ExternalStorageServiceException if the session fails to start
      * @throws IllegalStateException if a session has already been created for {@code vol}
      */
-    public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)
+    public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol)
             throws ExternalStorageServiceException {
         if (!shouldHandle(vol)) {
             return;
@@ -144,7 +145,8 @@
      *
      * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
      */
-    public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException {
+    public void notifyVolumeStateChanged(ImmutableVolumeInfo vol)
+            throws ExternalStorageServiceException {
         if (!shouldHandle(vol)) {
             return;
         }
@@ -214,7 +216,7 @@
      * @return the connection that was removed or {@code null} if nothing was removed
      */
     @Nullable
-    public StorageUserConnection onVolumeRemove(VolumeInfo vol) {
+    public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) {
         if (!shouldHandle(vol)) {
             return null;
         }
@@ -246,7 +248,7 @@
      *
      * Call {@link #onVolumeRemove} to remove the connection without waiting for exit
      */
-    public void onVolumeUnmount(VolumeInfo vol) {
+    public void onVolumeUnmount(ImmutableVolumeInfo vol) {
         String sessionId = vol.getId();
         final long token = Binder.clearCallingIdentity();
         try {
@@ -457,9 +459,9 @@
      * Returns {@code true} if {@code vol} is an emulated or visible public volume,
      * {@code false} otherwise
      **/
-    public static boolean isEmulatedOrPublic(VolumeInfo vol) {
-        return vol.type == VolumeInfo.TYPE_EMULATED
-                || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
+    public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) {
+        return vol.getType() == VolumeInfo.TYPE_EMULATED
+                || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
     }
 
     /** Exception thrown when communication with the {@link ExternalStorageService} fails. */
@@ -477,11 +479,11 @@
         }
     }
 
-    private static boolean isSupportedVolume(VolumeInfo vol) {
-        return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB;
+    private static boolean isSupportedVolume(ImmutableVolumeInfo vol) {
+        return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB;
     }
 
-    private boolean shouldHandle(@Nullable VolumeInfo vol) {
+    private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) {
         return !mIsResetting && (vol == null || isSupportedVolume(vol));
     }
 
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
new file mode 100644
index 0000000..4124cfb
--- /dev/null
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.storage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.utils.Watchable;
+import com.android.server.utils.WatchableImpl;
+
+import java.io.File;
+
+/**
+ * A wrapper for {@link VolumeInfo}  implementing the {@link Watchable} interface.
+ *
+ * The {@link VolumeInfo} class itself cannot safely implement Watchable, because it has several
+ * UnsupportedAppUsage annotations and public fields, which allow it to be modified without
+ * notifying watchers.
+ *
+ * @hide
+ */
+public class WatchedVolumeInfo extends WatchableImpl {
+    private final VolumeInfo mVolumeInfo;
+
+    private WatchedVolumeInfo(VolumeInfo volumeInfo) {
+        mVolumeInfo = volumeInfo;
+    }
+
+    public WatchedVolumeInfo(WatchedVolumeInfo watchedVolumeInfo) {
+        mVolumeInfo = new VolumeInfo(watchedVolumeInfo.mVolumeInfo);
+    }
+
+    public static WatchedVolumeInfo fromVolumeInfo(VolumeInfo info) {
+        return new WatchedVolumeInfo(info);
+    }
+
+    /**
+     * Returns a copy of the embedded VolumeInfo object, to be used by components
+     * that just need it for retrieving some state from it.
+     *
+     * @return A copy of the embedded VolumeInfo object
+     */
+
+    public WatchedVolumeInfo clone() {
+        return fromVolumeInfo(mVolumeInfo.clone());
+    }
+
+    public ImmutableVolumeInfo getImmutableVolumeInfo() {
+        return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
+    }
+
+    public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
+        return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        mVolumeInfo.dump(pw);
+    }
+
+    public DiskInfo getDisk() {
+        return mVolumeInfo.getDisk();
+    }
+
+    public String getDiskId() {
+        return mVolumeInfo.getDiskId();
+    }
+
+    public String getFsLabel() {
+        return mVolumeInfo.fsLabel;
+    }
+
+    public void setFsLabel(String fsLabel) {
+        mVolumeInfo.fsLabel = fsLabel;
+        dispatchChange(this);
+    }
+
+    public String getFsPath() {
+        return mVolumeInfo.path;
+    }
+
+    public void setFsPath(String path) {
+        mVolumeInfo.path = path;
+        dispatchChange(this);
+    }
+
+    public String getFsType() {
+        return mVolumeInfo.fsType;
+    }
+
+    public void setFsType(String fsType) {
+        mVolumeInfo.fsType = fsType;
+        dispatchChange(this);
+    }
+
+    public @Nullable String getFsUuid() {
+        return mVolumeInfo.fsUuid;
+    }
+
+    public void setFsUuid(String fsUuid) {
+        mVolumeInfo.fsUuid = fsUuid;
+        dispatchChange(this);
+    }
+
+    public @NonNull String getId() {
+        return mVolumeInfo.id;
+    }
+
+    public File getInternalPath() {
+        return mVolumeInfo.getInternalPath();
+    }
+
+    public void setInternalPath(String internalPath) {
+        mVolumeInfo.internalPath = internalPath;
+        dispatchChange(this);
+    }
+
+    public int getMountFlags() {
+        return mVolumeInfo.mountFlags;
+    }
+
+    public void setMountFlags(int mountFlags) {
+        mVolumeInfo.mountFlags = mountFlags;
+        dispatchChange(this);
+    }
+
+    public int getMountUserId() {
+        return mVolumeInfo.mountUserId;
+    }
+
+    public void setMountUserId(int mountUserId) {
+        mVolumeInfo.mountUserId = mountUserId;
+        dispatchChange(this);
+    }
+
+    public String getPartGuid() {
+        return mVolumeInfo.partGuid;
+    }
+
+    public File getPath() {
+        return mVolumeInfo.getPath();
+    }
+
+    public int getState() {
+        return mVolumeInfo.state;
+    }
+
+    public int getState(int state) {
+        return mVolumeInfo.state;
+    }
+
+    public void setState(int state) {
+        mVolumeInfo.state = state;
+        dispatchChange(this);
+    }
+
+    public int getType() {
+        return mVolumeInfo.type;
+    }
+
+    public VolumeInfo getVolumeInfo() {
+        return new VolumeInfo(mVolumeInfo);
+    }
+
+    public boolean isMountedReadable() {
+        return mVolumeInfo.isMountedReadable();
+    }
+
+    public boolean isMountedWritable() {
+        return mVolumeInfo.isMountedWritable();
+    }
+
+    public boolean isPrimary() {
+        return mVolumeInfo.isPrimary();
+    }
+
+    public boolean isVisible() {
+        return mVolumeInfo.isVisible();
+    }
+
+    public boolean isVisibleForUser(int userId) {
+        return mVolumeInfo.isVisibleForUser(userId);
+    }
+
+    public boolean isVisibleForWrite(int userId) {
+        return mVolumeInfo.isVisibleForWrite(userId);
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b17eef8..cf9c57a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -113,7 +113,6 @@
 import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED;
 import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING;
 import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
 import static android.view.WindowManager.hasWindowExtensionsEnabled;
@@ -246,9 +245,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
-import static com.android.server.wm.WindowManagerService.sEnableShellTransitions;
 import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
 
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.END_TAG;
@@ -660,8 +657,6 @@
 
     private RemoteAnimationDefinition mRemoteAnimationDefinition;
 
-    AnimatingActivityRegistry mAnimatingActivityRegistry;
-
     // Set to the previous Task parent of the ActivityRecord when it is reparented to a new Task
     // due to picture-in-picture. This gets cleared whenever this activity or the Task
     // it references to gets removed. This should also be cleared when we move out of pip.
@@ -764,13 +759,6 @@
     boolean mLastImeShown;
 
     /**
-     * When set to true, the IME insets will be frozen until the next app becomes IME input target.
-     * @see InsetsPolicy#adjustVisibilityForIme
-     * @see ImeInsetsSourceProvider#updateClientVisibility
-     */
-    boolean mImeInsetsFrozenUntilStartInput;
-
-    /**
      * A flag to determine if this AR is in the process of closing or entering PIP. This is needed
      * to help AR know that the app is in the process of closing but hasn't yet started closing on
      * the WM side.
@@ -865,12 +853,6 @@
     })
     @interface SplashScreenBehavior { }
 
-    // Force an app transition to be ran in the case the visibility of the app did not change.
-    // We use this for the case of moving a Root Task to the back with multiple activities, and the
-    // top activity enters PIP; the bottom activity's visibility stays the same, but we need to
-    // run the transition.
-    boolean mRequestForceTransition;
-
     boolean mEnteringAnimation;
     boolean mOverrideTaskTransition;
     boolean mDismissKeyguardIfInsecure;
@@ -1175,8 +1157,6 @@
                 pw.print(" launchMode="); pw.println(launchMode);
         pw.print(prefix); pw.print("mActivityType=");
                 pw.println(activityTypeToString(getActivityType()));
-        pw.print(prefix); pw.print("mImeInsetsFrozenUntilStartInput=");
-                pw.println(mImeInsetsFrozenUntilStartInput);
         if (requestedVrComponent != null) {
             pw.print(prefix);
             pw.print("requestedVrComponent=");
@@ -1597,9 +1577,6 @@
             }
         }
         final Task rootTask = getRootTask();
-
-        updateAnimatingActivityRegistry();
-
         if (task == mLastParentBeforePip && task != null) {
             // Notify the TaskFragmentOrganizer that the activity is reparented back from pip.
             mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController
@@ -1701,20 +1678,6 @@
         return !organizedTaskFragment.isAllowedToEmbedActivityInTrustedMode(this);
     }
 
-    void updateAnimatingActivityRegistry() {
-        final Task rootTask = getRootTask();
-        final AnimatingActivityRegistry registry = rootTask != null
-                ? rootTask.getAnimatingActivityRegistry()
-                : null;
-
-        // If we reparent, make sure to remove ourselves from the old animation registry.
-        if (mAnimatingActivityRegistry != null && mAnimatingActivityRegistry != registry) {
-            mAnimatingActivityRegistry.notifyFinished(this);
-        }
-
-        mAnimatingActivityRegistry = registry;
-    }
-
     boolean canAutoEnterPip() {
         // beforeStopping=false since the actual pip-ing will take place after startPausing()
         final boolean activityCanPip = checkEnterPictureInPictureState(
@@ -1799,7 +1762,6 @@
         if (prevDc.mOpeningApps.remove(this)) {
             // Transfer opening transition to new display.
             mDisplayContent.mOpeningApps.add(this);
-            mDisplayContent.transferAppTransitionFrom(prevDc);
             mDisplayContent.executeAppTransition();
         }
 
@@ -4642,12 +4604,6 @@
                 }
             }
 
-            // In this case, the starting icon has already been displayed, so start
-            // letting windows get shown immediately without any more transitions.
-            if (fromActivity.mVisible) {
-                mDisplayContent.mSkipAppTransitionAnimation = true;
-            }
-
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Moving existing starting %s"
                     + " from %s to %s", tStartingWindow, fromActivity, this);
 
@@ -5239,7 +5195,8 @@
                         pendingOptions.getWidth(), pendingOptions.getHeight());
                 options = AnimationOptions.makeScaleUpAnimOptions(
                         pendingOptions.getStartX(), pendingOptions.getStartY(),
-                        pendingOptions.getWidth(), pendingOptions.getHeight());
+                        pendingOptions.getWidth(), pendingOptions.getHeight(),
+                        pendingOptions.getOverrideTaskTransition());
                 if (intent.getSourceBounds() == null) {
                     intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
                             pendingOptions.getStartY(),
@@ -5677,76 +5634,17 @@
             mTransitionController.mValidateCommitVis.add(this);
             return;
         }
-        // If we are preparing an app transition, then delay changing
-        // the visibility of this token until we execute that transition.
-        if (deferCommitVisibilityChange(visible)) {
-            return;
-        }
 
         commitVisibility(visible, true /* performLayout */);
         updateReportedVisibilityLocked();
     }
 
-    /**
-     * Returns {@code true} if this activity is either added to opening-apps or closing-apps.
-     * Then its visibility will be committed until the transition is ready.
-     */
-    private boolean deferCommitVisibilityChange(boolean visible) {
-        if (mTransitionController.isShellTransitionsEnabled()) {
-            // Shell transition doesn't use opening/closing sets.
-            return false;
-        }
-        if (!mDisplayContent.mAppTransition.isTransitionSet()) {
-            return false;
-        }
-        if (mWaitForEnteringPinnedMode && mVisible == visible) {
-            // If the visibility is not changed during enter PIP, we don't want to include it in
-            // app transition to affect the animation theme, because the Pip organizer will
-            // animate the entering PIP instead.
-            return false;
-        }
-
-        // The animation will be visible soon so do not skip by screen off.
-        final boolean ignoreScreenOn = canTurnScreenOn() || mTaskSupervisor.getKeyguardController()
-                .isKeyguardGoingAway(mDisplayContent.mDisplayId);
-        // Ignore display frozen so the opening / closing transition type can be updated correctly
-        // even if the display is frozen. And it's safe since in applyAnimation will still check
-        // DC#okToAnimate again if the transition animation is fine to apply.
-        if (!okToAnimate(true /* ignoreFrozen */, ignoreScreenOn)) {
-            return false;
-        }
-        if (visible) {
-            mDisplayContent.mOpeningApps.add(this);
-            mEnteringAnimation = true;
-        } else if (mVisible) {
-            mDisplayContent.mClosingApps.add(this);
-            mEnteringAnimation = false;
-        }
-        if ((mDisplayContent.mAppTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) {
-            // Add the launching-behind activity to mOpeningApps.
-            final WindowState win = mDisplayContent.findFocusedWindow();
-            if (win != null) {
-                final ActivityRecord focusedActivity = win.mActivityRecord;
-                if (focusedActivity != null) {
-                    ProtoLog.d(WM_DEBUG_APP_TRANSITIONS,
-                            "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
-                            focusedActivity);
-                    // Force animation to be loaded.
-                    mDisplayContent.mOpeningApps.add(focusedActivity);
-                }
-            }
-        }
-        return true;
-    }
-
     @Override
     boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter,
             boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) {
         if ((mTransitionChangeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
             return false;
         }
-        // If it was set to true, reset the last request to force the transition.
-        mRequestForceTransition = false;
         return super.applyAnimation(lp, transit, enter, isVoiceInteraction, sources);
     }
 
@@ -5771,19 +5669,16 @@
             return;
         }
 
-        final int windowsCount = mChildren.size();
-        // With Shell-Transition, the activity will running a transition when it is visible.
-        // It won't be included when fromTransition is true means the call from finishTransition.
-        final boolean runningAnimation = sEnableShellTransitions ? visible
-                : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION);
-        for (int i = 0; i < windowsCount; i++) {
-            mChildren.get(i).onAppVisibilityChanged(visible, runningAnimation);
+        if (!visible) {
+            for (int i = mChildren.size() - 1; i >= 0; --i) {
+                mChildren.get(i).onAppCommitInvisible();
+            }
         }
         setVisible(visible);
         setVisibleRequested(visible);
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b"
-                        + " visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s",
-                this, isVisible(), mVisibleRequested, isInTransition(), runningAnimation,
+                        + " visibleRequested=%b, inTransition=%b, caller=%s",
+                this, visible, mVisibleRequested, inTransition(),
                 Debug.getCallers(5));
         if (visible) {
             // If we are being set visible, and the starting window is not yet displayed,
@@ -5873,10 +5768,6 @@
         }
 
         final DisplayContent displayContent = getDisplayContent();
-        if (!visible) {
-            mImeInsetsFrozenUntilStartInput = true;
-        }
-
         if (!displayContent.mClosingApps.contains(this)
                 && !displayContent.mOpeningApps.contains(this)
                 && !fromTransition) {
@@ -5916,27 +5807,6 @@
     }
 
     /**
-     * Check if visibility of this {@link ActivityRecord} should be updated as part of an app
-     * transition.
-     *
-     * <p class="note><strong>Note:</strong> If the visibility of this {@link ActivityRecord} is
-     * already set to {@link #mVisible}, we don't need to update the visibility. So {@code false} is
-     * returned.</p>
-     *
-     * @param visible {@code true} if this {@link ActivityRecord} should become visible,
-     *                {@code false} if this should become invisible.
-     * @return {@code true} if visibility of this {@link ActivityRecord} should be updated, and
-     *         an app transition animation should be run.
-     */
-    boolean shouldApplyAnimation(boolean visible) {
-        // Allow for state update and animation to be applied if:
-        // * activity is transitioning visibility state
-        // * or the activity was marked as hidden and is exiting before we had a chance to play the
-        // transition animation
-        return isVisible() != visible || mRequestForceTransition || (!isVisible() && mIsExiting);
-    }
-
-    /**
      * See {@link Activity#setRecentsScreenshotEnabled}.
      */
     void setRecentsScreenshotEnabled(boolean enabled) {
@@ -6224,13 +6094,8 @@
             return false;
         }
 
-        // Hide all activities on the presenting display so that malicious apps can't do tap
-        // jacking (b/391466268).
-        // For now, this should only be applied to external displays because presentations can only
-        // be shown on them.
-        // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
-        // the presentation won't stop its controlling activity.
-        if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) {
+        // A presentation stopps all activities behind on the same display.
+        if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) {
             return false;
         }
 
@@ -6952,14 +6817,6 @@
             // closing activity having to wait until idle timeout to be stopped or destroyed if the
             // next activity won't report idle (e.g. repeated view animation).
             mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded();
-
-            // If the activity is visible, but no windows are eligible to start input, unfreeze
-            // to avoid permanently frozen IME insets.
-            if (mImeInsetsFrozenUntilStartInput && getWindow(
-                    win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags))
-                    == null) {
-                mImeInsetsFrozenUntilStartInput = false;
-            }
         }
     }
 
@@ -7659,13 +7516,6 @@
     }
 
     @Override
-    public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
-        return mAnimatingActivityRegistry != null
-                && mAnimatingActivityRegistry.notifyAboutToFinish(
-                this, endDeferFinishCallback);
-    }
-
-    @Override
     boolean isWaitingForTransitionStart() {
         final DisplayContent dc = getDisplayContent();
         return dc != null && dc.mAppTransition.isTransitionSet()
@@ -7686,10 +7536,6 @@
 
     @Override
     public void onLeashAnimationStarting(Transaction t, SurfaceControl leash) {
-        if (mAnimatingActivityRegistry != null) {
-            mAnimatingActivityRegistry.notifyStarting(this);
-        }
-
         if (mNeedsLetterboxedAnimation) {
             updateLetterboxSurfaceIfNeeded(findMainWindow(), t);
             mNeedsAnimationBoundsLayer = true;
@@ -7700,17 +7546,7 @@
         // new layer.
         if (mNeedsAnimationBoundsLayer) {
             mTmpRect.setEmpty();
-            if (getDisplayContent().mAppTransitionController.isTransitWithinTask(
-                    getTransit(), task)) {
-                task.getBounds(mTmpRect);
-            } else {
-                final Task rootTask = getRootTask();
-                if (rootTask == null) {
-                    return;
-                }
-                // Set clip rect to root task bounds.
-                rootTask.getBounds(mTmpRect);
-            }
+            task.getBounds(mTmpRect);
             mAnimationBoundsLayer = createAnimationBoundsLayer(t);
 
             // Crop to root task bounds.
@@ -7866,10 +7702,6 @@
             mNeedsLetterboxedAnimation = false;
             updateLetterboxSurfaceIfNeeded(findMainWindow(), t);
         }
-
-        if (mAnimatingActivityRegistry != null) {
-            mAnimatingActivityRegistry.notifyFinished(this);
-        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index ddb9f17..819e117 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4128,22 +4128,7 @@
     @Override
     public void registerRemoteAnimationsForDisplay(int displayId,
             RemoteAnimationDefinition definition) {
-        mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
-                "registerRemoteAnimations");
-        definition.setCallingPidUid(Binder.getCallingPid(), Binder.getCallingUid());
-        synchronized (mGlobalLock) {
-            final DisplayContent display = mRootWindowContainer.getDisplayContent(displayId);
-            if (display == null) {
-                Slog.e(TAG, "Couldn't find display with id: " + displayId);
-                return;
-            }
-            final long origId = Binder.clearCallingIdentity();
-            try {
-                display.registerRemoteAnimations(definition);
-            } finally {
-                Binder.restoreCallingIdentity(origId);
-            }
-        }
+        // TODO(b/365884835): Remove callers.
     }
 
     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
@@ -4324,10 +4309,12 @@
                 task = mRootWindowContainer.getDefaultTaskDisplayArea().getRootTask(
                         t -> t.isActivityTypeStandard());
             }
-            if (task != null && task.getTopMostActivity() != null
-                    && !task.getTopMostActivity().isState(FINISHING, DESTROYING, DESTROYED)) {
+            final ActivityRecord topActivity = task != null
+                    ? task.getTopMostActivity()
+                    : null;
+            if (topActivity != null && !topActivity.isState(FINISHING, DESTROYING, DESTROYED)) {
                 mWindowManager.mAtmService.mActivityClientController
-                        .onPictureInPictureUiStateChanged(task.getTopMostActivity(), pipState);
+                        .onPictureInPictureUiStateChanged(topActivity, pipState);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java b/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java
deleted file mode 100644
index 18ec96c..0000000
--- a/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.wm;
-
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-
-/**
- * Keeps track of all {@link ActivityRecord} that are animating and makes sure all animations are
- * finished at the same time such that we don't run into issues with z-ordering: An activity A
- * that has a shorter animation that is above another activity B with a longer animation in the same
- * task, the animation layer would put the B on top of A, but from the hierarchy, A needs to be on
- * top of B. Thus, we defer reparenting A to the original hierarchy such that it stays on top of B
- * until B finishes animating.
- */
-class AnimatingActivityRegistry {
-
-    private ArraySet<ActivityRecord> mAnimatingActivities = new ArraySet<>();
-    private ArrayMap<ActivityRecord, Runnable> mFinishedTokens = new ArrayMap<>();
-
-    private ArrayList<Runnable> mTmpRunnableList = new ArrayList<>();
-
-    private boolean mEndingDeferredFinish;
-
-    /**
-     * Notifies that an {@link ActivityRecord} has started animating.
-     */
-    void notifyStarting(ActivityRecord token) {
-        mAnimatingActivities.add(token);
-    }
-
-    /**
-     * Notifies that an {@link ActivityRecord} has finished animating.
-     */
-    void notifyFinished(ActivityRecord activity) {
-        mAnimatingActivities.remove(activity);
-        mFinishedTokens.remove(activity);
-
-        // If we were the last activity, make sure the end all deferred finishes.
-        if (mAnimatingActivities.isEmpty()) {
-            endDeferringFinished();
-        }
-    }
-
-    /**
-     * Called when an {@link ActivityRecord} is about to finish animating.
-     *
-     * @param endDeferFinishCallback Callback to run when defer finish should be ended.
-     * @return {@code true} if finishing the animation should be deferred, {@code false} otherwise.
-     */
-    boolean notifyAboutToFinish(ActivityRecord activity, Runnable endDeferFinishCallback) {
-        final boolean removed = mAnimatingActivities.remove(activity);
-        if (!removed) {
-            return false;
-        }
-
-        if (mAnimatingActivities.isEmpty()) {
-
-            // If no animations are animating anymore, finish all others.
-            endDeferringFinished();
-            return false;
-        } else {
-
-            // Otherwise let's put it into the pending list of to be finished animations.
-            mFinishedTokens.put(activity, endDeferFinishCallback);
-            return true;
-        }
-    }
-
-    private void endDeferringFinished() {
-
-        // Don't start recursing. Running the finished listener invokes notifyFinished, which may
-        // invoked us again.
-        if (mEndingDeferredFinish) {
-            return;
-        }
-        try {
-            mEndingDeferredFinish = true;
-
-            // Copy it into a separate temp list to avoid modifying the collection while iterating
-            // as calling the callback may call back into notifyFinished.
-            for (int i = mFinishedTokens.size() - 1; i >= 0; i--) {
-                mTmpRunnableList.add(mFinishedTokens.valueAt(i));
-            }
-            mFinishedTokens.clear();
-            for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) {
-                mTmpRunnableList.get(i).run();
-            }
-            mTmpRunnableList.clear();
-        } finally {
-            mEndingDeferredFinish = false;
-        }
-    }
-
-    void dump(PrintWriter pw, String header, String prefix) {
-        if (!mAnimatingActivities.isEmpty() || !mFinishedTokens.isEmpty()) {
-            pw.print(prefix); pw.println(header);
-            prefix = prefix + "  ";
-            pw.print(prefix); pw.print("mAnimatingActivities="); pw.println(mAnimatingActivities);
-            pw.print(prefix); pw.print("mFinishedTokens="); pw.println(mFinishedTokens);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/wm/AnimationAdapter.java b/services/core/java/com/android/server/wm/AnimationAdapter.java
index 3dc377d..4458ed7 100644
--- a/services/core/java/com/android/server/wm/AnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/AnimationAdapter.java
@@ -109,15 +109,6 @@
      * Gets called when the animation is about to finish and gives the client the opportunity to
      * defer finishing the animation, i.e. it keeps the leash around until the client calls
      * endDeferFinishCallback.
-     * <p>
-     * This has the same effect as
-     * {@link com.android.server.wm.SurfaceAnimator.Animatable#shouldDeferAnimationFinish(Runnable)}
-     * . The later will be evaluated first and has precedence over this method if it returns true,
-     * which means that if the {@link com.android.server.wm.SurfaceAnimator.Animatable} requests to
-     * defer its finish, this method won't be called so this adapter will never have access to the
-     * finish callback. On the other hand, if the
-     * {@link com.android.server.wm.SurfaceAnimator.Animatable}, doesn't request to defer, this
-     * {@link AnimationAdapter} is responsible for ending the animation.
      *
      * @param endDeferFinishCallback The callback to call when defer finishing should be ended.
      * @return Whether the client would like to defer the animation finish.
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 932f268..9c4b722 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -1462,7 +1462,7 @@
     }
 
     boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) {
-        if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
+        if (WindowManagerService.sEnableShellTransitions) {
             return false;
         }
         mNextAppTransitionRequests.add(transit);
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
deleted file mode 100644
index d5fe056..0000000
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ /dev/null
@@ -1,1352 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.wm;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
-import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_NONE;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_RELAUNCH;
-import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_OLD_NONE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_FRONT;
-import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_OPEN;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_RELAUNCH;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS;
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM;
-import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
-import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
-import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SNAPSHOT;
-import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
-import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
-import static com.android.server.wm.AppTransition.isNormalTransit;
-import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp;
-import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation;
-import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-
-import android.annotation.IntDef;
-import android.annotation.Nullable;
-import android.graphics.Rect;
-import android.os.Trace;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Pair;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationDefinition;
-import android.view.WindowManager;
-import android.view.WindowManager.LayoutParams;
-import android.view.WindowManager.TransitionFlags;
-import android.view.WindowManager.TransitionOldType;
-import android.view.WindowManager.TransitionType;
-import android.window.ITaskFragmentOrganizer;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLog;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * Checks for app transition readiness, resolves animation attributes and performs visibility
- * change for apps that animate as part of an app transition.
- */
-public class AppTransitionController {
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "AppTransitionController" : TAG_WM;
-    private final WindowManagerService mService;
-    private final DisplayContent mDisplayContent;
-    private final WallpaperController mWallpaperControllerLocked;
-    private RemoteAnimationDefinition mRemoteAnimationDefinition = null;
-
-    private static final int TYPE_NONE = 0;
-    private static final int TYPE_ACTIVITY = 1;
-    private static final int TYPE_TASK_FRAGMENT = 2;
-    private static final int TYPE_TASK = 3;
-
-    @IntDef(prefix = { "TYPE_" }, value = {
-            TYPE_NONE,
-            TYPE_ACTIVITY,
-            TYPE_TASK_FRAGMENT,
-            TYPE_TASK
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface TransitContainerType {}
-
-    private final ArrayMap<WindowContainer, Integer> mTempTransitionReasons = new ArrayMap<>();
-    private final ArrayList<WindowContainer> mTempTransitionWindows = new ArrayList<>();
-
-    AppTransitionController(WindowManagerService service, DisplayContent displayContent) {
-        mService = service;
-        mDisplayContent = displayContent;
-        mWallpaperControllerLocked = mDisplayContent.mWallpaperController;
-    }
-
-    void registerRemoteAnimations(RemoteAnimationDefinition definition) {
-        mRemoteAnimationDefinition = definition;
-    }
-
-    /**
-     * Returns the currently visible window that is associated with the wallpaper in case we are
-     * transitioning from an activity with a wallpaper to one without.
-     */
-    @Nullable
-    private WindowState getOldWallpaper() {
-        final WindowState wallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget();
-        final @TransitionType int firstTransit =
-                mDisplayContent.mAppTransition.getFirstAppTransition();
-
-        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
-                mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, true /* visible */);
-        final boolean showWallpaper = wallpaperTarget != null
-                && (wallpaperTarget.hasWallpaper()
-                // Update task open transition to wallpaper transition when wallpaper is visible.
-                // (i.e.launching app info activity from recent tasks)
-                || ((firstTransit == TRANSIT_OPEN || firstTransit == TRANSIT_TO_FRONT)
-                && (!openingWcs.isEmpty() && openingWcs.valueAt(0).asTask() != null)
-                && mWallpaperControllerLocked.isWallpaperVisible()));
-        // If wallpaper is animating or wallpaperTarget doesn't have SHOW_WALLPAPER flag set,
-        // don't consider upgrading to wallpaper transition.
-        return (mWallpaperControllerLocked.isWallpaperTargetAnimating() || !showWallpaper)
-                ? null : wallpaperTarget;
-    }
-
-    /**
-     * Handle application transition for given display.
-     */
-    void handleAppTransitionReady() {
-        mTempTransitionReasons.clear();
-        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
-                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
-                || !transitionGoodToGoForTaskFragments()) {
-            return;
-        }
-
-        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");
-
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO");
-        // TODO(b/205335975): Remove window which stuck in animatingExit status. Find actual cause.
-        mDisplayContent.forAllWindows(WindowState::cleanupAnimatingExitWindow,
-                true /* traverseTopToBottom */);
-        // TODO(new-app-transition): Remove code using appTransition.getAppTransition()
-        final AppTransition appTransition = mDisplayContent.mAppTransition;
-
-        mDisplayContent.mNoAnimationNotifyOnTransitionFinished.clear();
-
-        appTransition.removeAppTransitionTimeoutCallbacks();
-
-        mDisplayContent.mWallpaperMayChange = false;
-
-        int appCount = mDisplayContent.mOpeningApps.size();
-        for (int i = 0; i < appCount; ++i) {
-            // Clearing the mAnimatingExit flag before entering animation. It's set to true if app
-            // window is removed, or window relayout to invisible. This also affects window
-            // visibility. We need to clear it *before* maybeUpdateTransitToWallpaper() as the
-            // transition selection depends on wallpaper target visibility.
-            mDisplayContent.mOpeningApps.valueAtUnchecked(i).clearAnimatingFlags();
-        }
-        appCount = mDisplayContent.mChangingContainers.size();
-        for (int i = 0; i < appCount; ++i) {
-            // Clearing for same reason as above.
-            final ActivityRecord activity = getAppFromContainer(
-                    mDisplayContent.mChangingContainers.valueAtUnchecked(i));
-            if (activity != null) {
-                activity.clearAnimatingFlags();
-            }
-        }
-
-        // Adjust wallpaper before we pull the lower/upper target, since pending changes
-        // (like the clearAnimatingFlags() above) might affect wallpaper target result.
-        // Or, the opening app window should be a wallpaper target.
-        mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(
-                mDisplayContent.mOpeningApps);
-
-        ArraySet<ActivityRecord> tmpOpenApps = mDisplayContent.mOpeningApps;
-        ArraySet<ActivityRecord> tmpCloseApps = mDisplayContent.mClosingApps;
-        if (mDisplayContent.mAtmService.mBackNavigationController.isMonitoringFinishTransition()) {
-            tmpOpenApps = new ArraySet<>(mDisplayContent.mOpeningApps);
-            tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps);
-        }
-
-        @TransitionOldType final int transit = getTransitCompatType(
-                mDisplayContent.mAppTransition, tmpOpenApps,
-                tmpCloseApps, mDisplayContent.mChangingContainers,
-                mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
-                mDisplayContent.mSkipAppTransitionAnimation);
-        mDisplayContent.mSkipAppTransitionAnimation = false;
-
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                "handleAppTransitionReady: displayId=%d appTransition={%s}"
-                + " openingApps=[%s] closingApps=[%s] transit=%s",
-                mDisplayContent.mDisplayId, appTransition.toString(), tmpOpenApps,
-                tmpCloseApps, AppTransition.appTransitionOldToString(transit));
-
-        // Find the layout params of the top-most application window in the tokens, which is
-        // what will control the animation theme. If all closing windows are obscured, then there is
-        // no need to do an animation. This is the case, for example, when this transition is being
-        // done behind a dream window.
-        final ArraySet<Integer> activityTypes = collectActivityTypes(tmpOpenApps,
-                tmpCloseApps, mDisplayContent.mChangingContainers);
-        final ActivityRecord animLpActivity = findAnimLayoutParamsToken(transit, activityTypes,
-                tmpOpenApps, tmpCloseApps, mDisplayContent.mChangingContainers);
-        final ActivityRecord topOpeningApp =
-                getTopApp(tmpOpenApps, false /* ignoreHidden */);
-        final ActivityRecord topClosingApp =
-                getTopApp(tmpCloseApps, false /* ignoreHidden */);
-        final ActivityRecord topChangingApp =
-                getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
-        final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);
-
-        // Check if there is any override
-        if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) {
-            // Unfreeze the windows that were previously frozen for TaskFragment animation.
-            overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
-        }
-
-        final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mClosingApps)
-                || containsVoiceInteraction(mDisplayContent.mOpeningApps);
-
-        final int layoutRedo;
-        mService.mSurfaceAnimationRunner.deferStartingAnimations();
-        try {
-            applyAnimations(tmpOpenApps, tmpCloseApps, transit, animLp, voiceInteraction);
-            handleClosingApps();
-            handleOpeningApps();
-            handleChangingApps(transit);
-            handleClosingChangingContainers();
-
-            appTransition.setLastAppTransition(transit, topOpeningApp,
-                    topClosingApp, topChangingApp);
-
-            final int flags = appTransition.getTransitFlags();
-            layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
-            appTransition.postAnimationCallback();
-        } finally {
-            appTransition.clear();
-            mService.mSurfaceAnimationRunner.continueStartingAnimations();
-        }
-
-        mService.mSnapshotController.onTransitionStarting(mDisplayContent);
-
-        mDisplayContent.mOpeningApps.clear();
-        mDisplayContent.mClosingApps.clear();
-        mDisplayContent.mChangingContainers.clear();
-        mDisplayContent.mUnknownAppVisibilityController.clear();
-        mDisplayContent.mClosingChangingContainers.clear();
-
-        // This has changed the visibility of windows, so perform
-        // a new layout to get them all up-to-date.
-        mDisplayContent.setLayoutNeeded();
-
-        mDisplayContent.computeImeTarget(true /* updateImeTarget */);
-
-        mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting(
-                mTempTransitionReasons);
-
-        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
-
-        mDisplayContent.pendingLayoutChanges |=
-                layoutRedo | FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_CONFIG;
-    }
-
-    /**
-     * Get old transit type based on the current transit requests.
-     *
-     * @param appTransition {@link AppTransition} for managing app transition state.
-     * @param openingApps {@link ActivityRecord}s which are becoming visible.
-     * @param closingApps {@link ActivityRecord}s which are becoming invisible.
-     * @param changingContainers {@link WindowContainer}s which are changed in configuration.
-     * @param wallpaperTarget If non-null, this is the currently visible window that is associated
-     *                        with the wallpaper.
-     * @param oldWallpaper The currently visible window that is associated with the wallpaper in
-     *                     case we are transitioning from an activity with a wallpaper to one
-     *                     without. Otherwise null.
-     */
-    @TransitionOldType static int getTransitCompatType(AppTransition appTransition,
-            ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
-            ArraySet<WindowContainer> changingContainers, @Nullable WindowState wallpaperTarget,
-            @Nullable WindowState oldWallpaper, boolean skipAppTransitionAnimation) {
-
-        final ActivityRecord topOpeningApp = getTopApp(openingApps, false /* ignoreHidden */);
-        final ActivityRecord topClosingApp = getTopApp(closingApps, true /* ignoreHidden */);
-
-        // Determine if closing and opening app token sets are wallpaper targets, in which case
-        // special animations are needed.
-        final boolean openingAppHasWallpaper = canBeWallpaperTarget(openingApps)
-                && wallpaperTarget != null;
-        final boolean closingAppHasWallpaper = canBeWallpaperTarget(closingApps)
-                && wallpaperTarget != null;
-
-        // Keyguard transit has high priority.
-        switch (appTransition.getKeyguardTransition()) {
-            case TRANSIT_KEYGUARD_GOING_AWAY:
-                return openingAppHasWallpaper ? TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER
-                        : TRANSIT_OLD_KEYGUARD_GOING_AWAY;
-            case TRANSIT_KEYGUARD_OCCLUDE:
-                // When there is a closing app, the keyguard has already been occluded by an
-                // activity, and another activity has started on top of that activity, so normal
-                // app transition animation should be used.
-                if (!closingApps.isEmpty()) {
-                    return TRANSIT_OLD_ACTIVITY_OPEN;
-                }
-                if (!openingApps.isEmpty() && openingApps.valueAt(0).getActivityType()
-                        == ACTIVITY_TYPE_DREAM) {
-                    return TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM;
-                }
-                return TRANSIT_OLD_KEYGUARD_OCCLUDE;
-            case TRANSIT_KEYGUARD_UNOCCLUDE:
-                return TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
-        }
-
-        // Determine whether the top opening and closing activity is a dream activity. If so, this
-        // has higher priority than others except keyguard transit.
-        if (topOpeningApp != null && topOpeningApp.getActivityType() == ACTIVITY_TYPE_DREAM) {
-            return TRANSIT_OLD_DREAM_ACTIVITY_OPEN;
-        } else if (topClosingApp != null
-                && topClosingApp.getActivityType() == ACTIVITY_TYPE_DREAM) {
-            return TRANSIT_OLD_DREAM_ACTIVITY_CLOSE;
-        }
-
-        // This is not keyguard transition and one of the app has request to skip app transition.
-        if (skipAppTransitionAnimation) {
-            return WindowManager.TRANSIT_OLD_UNSET;
-        }
-        @TransitionFlags final int flags = appTransition.getTransitFlags();
-        @TransitionType final int firstTransit = appTransition.getFirstAppTransition();
-
-        // Special transitions
-        // TODO(new-app-transitions): Revisit if those can be rewritten by using flags.
-        if (appTransition.containsTransitRequest(TRANSIT_CHANGE) && !changingContainers.isEmpty()) {
-            @TransitContainerType int changingType =
-                    getTransitContainerType(changingContainers.valueAt(0));
-            switch (changingType) {
-                case TYPE_TASK:
-                    return TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
-                case TYPE_TASK_FRAGMENT:
-                    return TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
-                default:
-                    throw new IllegalStateException(
-                            "TRANSIT_CHANGE with unrecognized changing type=" + changingType);
-            }
-        }
-        if ((flags & TRANSIT_FLAG_APP_CRASHED) != 0) {
-            return TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
-        }
-        if (firstTransit == TRANSIT_NONE) {
-            return TRANSIT_OLD_NONE;
-        }
-
-        /*
-         * There are cases where we open/close a new task/activity, but in reality only a
-         * translucent activity on top of existing activities is opening/closing. For that one, we
-         * have a different animation because non of the task/activity animations actually work well
-         * with translucent apps.
-         */
-        if (isNormalTransit(firstTransit)) {
-            boolean allOpeningVisible = true;
-            boolean allTranslucentOpeningApps = !openingApps.isEmpty();
-            for (int i = openingApps.size() - 1; i >= 0; i--) {
-                final ActivityRecord activity = openingApps.valueAt(i);
-                if (!activity.isVisible()) {
-                    allOpeningVisible = false;
-                    if (activity.fillsParent()) {
-                        allTranslucentOpeningApps = false;
-                    }
-                }
-            }
-            boolean allTranslucentClosingApps = !closingApps.isEmpty();
-            for (int i = closingApps.size() - 1; i >= 0; i--) {
-                if (closingApps.valueAt(i).fillsParent()) {
-                    allTranslucentClosingApps = false;
-                    break;
-                }
-            }
-
-            if (allTranslucentClosingApps && allOpeningVisible) {
-                return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE;
-            }
-            if (allTranslucentOpeningApps && closingApps.isEmpty()) {
-                return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN;
-            }
-        }
-
-        if (closingAppHasWallpaper && openingAppHasWallpaper) {
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Wallpaper animation!");
-            switch (firstTransit) {
-                case TRANSIT_OPEN:
-                case TRANSIT_TO_FRONT:
-                    return TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
-                case TRANSIT_CLOSE:
-                case TRANSIT_TO_BACK:
-                    return TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
-            }
-        } else if (oldWallpaper != null && !openingApps.isEmpty()
-                && !openingApps.contains(oldWallpaper.mActivityRecord)
-                && closingApps.contains(oldWallpaper.mActivityRecord)
-                && topClosingApp == oldWallpaper.mActivityRecord) {
-            // We are transitioning from an activity with a wallpaper to one without.
-            return TRANSIT_OLD_WALLPAPER_CLOSE;
-        } else if (wallpaperTarget != null && wallpaperTarget.isVisible()
-                && openingApps.contains(wallpaperTarget.mActivityRecord)
-                && topOpeningApp == wallpaperTarget.mActivityRecord
-                /* && transit != TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE */) {
-            // We are transitioning from an activity without
-            // a wallpaper to now showing the wallpaper
-            return TRANSIT_OLD_WALLPAPER_OPEN;
-        }
-
-        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
-                openingApps, closingApps, true /* visible */);
-        final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
-                openingApps, closingApps, false /* visible */);
-        final WindowContainer<?> openingContainer = !openingWcs.isEmpty()
-                ? openingWcs.valueAt(0) : null;
-        final WindowContainer<?> closingContainer = !closingWcs.isEmpty()
-                ? closingWcs.valueAt(0) : null;
-        @TransitContainerType int openingType = getTransitContainerType(openingContainer);
-        @TransitContainerType int closingType = getTransitContainerType(closingContainer);
-        if (appTransition.containsTransitRequest(TRANSIT_TO_FRONT) && openingType == TYPE_TASK) {
-            if (topOpeningApp != null && topOpeningApp.isActivityTypeHome()) {
-                // If we are opening the home task, we want to play an animation as if
-                // the task on top is being brought to back.
-                return TRANSIT_OLD_TASK_TO_BACK;
-            }
-            return TRANSIT_OLD_TASK_TO_FRONT;
-        }
-        if (appTransition.containsTransitRequest(TRANSIT_TO_BACK) && closingType == TYPE_TASK) {
-            return TRANSIT_OLD_TASK_TO_BACK;
-        }
-        if (appTransition.containsTransitRequest(TRANSIT_OPEN)) {
-            if (openingType == TYPE_TASK) {
-                return (appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0
-                        ? TRANSIT_OLD_TASK_OPEN_BEHIND : TRANSIT_OLD_TASK_OPEN;
-            }
-            if (openingType == TYPE_ACTIVITY) {
-                return TRANSIT_OLD_ACTIVITY_OPEN;
-            }
-            if (openingType == TYPE_TASK_FRAGMENT) {
-                return TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-            }
-        }
-        if (appTransition.containsTransitRequest(TRANSIT_CLOSE)) {
-            if (closingType == TYPE_TASK) {
-                return TRANSIT_OLD_TASK_CLOSE;
-            }
-            if (closingType == TYPE_TASK_FRAGMENT) {
-                return TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
-            }
-            if (closingType == TYPE_ACTIVITY) {
-                for (int i = closingApps.size() - 1; i >= 0; i--) {
-                    if (closingApps.valueAt(i).visibleIgnoringKeyguard) {
-                        return TRANSIT_OLD_ACTIVITY_CLOSE;
-                    }
-                }
-                // Skip close activity transition since no closing app can be visible
-                return WindowManager.TRANSIT_OLD_UNSET;
-            }
-        }
-        if (appTransition.containsTransitRequest(TRANSIT_RELAUNCH)
-                && !openingWcs.isEmpty() && !openingApps.isEmpty()) {
-            return TRANSIT_OLD_ACTIVITY_RELAUNCH;
-        }
-        return TRANSIT_OLD_NONE;
-    }
-
-    @TransitContainerType
-    private static int getTransitContainerType(@Nullable WindowContainer<?> container) {
-        if (container == null) {
-            return TYPE_NONE;
-        }
-        if (container.asTask() != null) {
-            return TYPE_TASK;
-        }
-        if (container.asTaskFragment() != null) {
-            return TYPE_TASK_FRAGMENT;
-        }
-        if (container.asActivityRecord() != null) {
-            return TYPE_ACTIVITY;
-        }
-        return TYPE_NONE;
-    }
-
-    @Nullable
-    private static WindowManager.LayoutParams getAnimLp(ActivityRecord activity) {
-        final WindowState mainWindow = activity != null ? activity.findMainWindow() : null;
-        return mainWindow != null ? mainWindow.mAttrs : null;
-    }
-
-    RemoteAnimationAdapter getRemoteAnimationOverride(@Nullable WindowContainer container,
-            @TransitionOldType int transit, ArraySet<Integer> activityTypes) {
-        if (container != null) {
-            final RemoteAnimationDefinition definition = container.getRemoteAnimationDefinition();
-            if (definition != null) {
-                final RemoteAnimationAdapter adapter = definition.getAdapter(transit,
-                        activityTypes);
-                if (adapter != null) {
-                    return adapter;
-                }
-            }
-        }
-        return mRemoteAnimationDefinition != null
-                ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes)
-                : null;
-    }
-
-    private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) {
-        // We don't want to have the client to animate any non-app windows.
-        // Having {@code transit} of those types doesn't mean it will contain non-app windows, but
-        // non-app windows will only be included with those transition types. And we don't currently
-        // have any use case of those for TaskFragment transition.
-        return shouldStartNonAppWindowAnimationsForKeyguardExit(transit)
-                || shouldAttachNavBarToApp(mService, mDisplayContent, transit)
-                || shouldStartWallpaperAnimation(mDisplayContent);
-    }
-
-    /**
-     * Whether the transition contains any embedded {@link TaskFragment} that does not fill the
-     * parent {@link Task} before or after the transition.
-     */
-    private boolean transitionContainsTaskFragmentWithBoundsOverride() {
-        for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
-            final WindowContainer wc = mDisplayContent.mChangingContainers.valueAt(i);
-            if (wc.isEmbedded()) {
-                // Contains embedded TaskFragment with bounds changed.
-                return true;
-            }
-        }
-        mTempTransitionWindows.clear();
-        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
-        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
-        boolean containsTaskFragmentWithBoundsOverride = false;
-        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
-            final ActivityRecord r = mTempTransitionWindows.get(i).asActivityRecord();
-            final TaskFragment tf = r.getTaskFragment();
-            if (tf != null && tf.isEmbeddedWithBoundsOverride()) {
-                containsTaskFragmentWithBoundsOverride = true;
-                break;
-            }
-        }
-        mTempTransitionWindows.clear();
-        return containsTaskFragmentWithBoundsOverride;
-    }
-
-    /**
-     * Finds the common parent {@link Task} that is parent of all embedded app windows in the
-     * current transition.
-     * @return {@code null} if app windows in the transition are not children of the same Task, or
-     *         if none of the app windows is embedded.
-     */
-    @Nullable
-    private Task findParentTaskForAllEmbeddedWindows() {
-        mTempTransitionWindows.clear();
-        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
-        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
-        mTempTransitionWindows.addAll(mDisplayContent.mChangingContainers);
-
-        // It should only animated by the organizer if all windows are below the same leaf Task.
-        Task leafTask = null;
-        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
-            final ActivityRecord r = getAppFromContainer(mTempTransitionWindows.get(i));
-            if (r == null) {
-                leafTask = null;
-                break;
-            }
-            // There are also cases where the Task contains non-embedded activity, such as launching
-            // split TaskFragments from a non-embedded activity.
-            // The hierarchy may looks like this:
-            // - Task
-            //    - Activity
-            //    - TaskFragment
-            //       - Activity
-            //    - TaskFragment
-            //       - Activity
-            // We also want to have the organizer handle the transition for such case.
-            final Task task = r.getTask();
-            // We don't support embedding in PiP, leave the animation to the PipTaskOrganizer.
-            if (task == null || task.inPinnedWindowingMode()) {
-                leafTask = null;
-                break;
-            }
-            // We don't want the organizer to handle transition of other non-embedded Task.
-            if (leafTask != null && leafTask != task) {
-                leafTask = null;
-                break;
-            }
-            final ActivityRecord rootActivity = task.getRootActivity();
-            // We don't want the organizer to handle transition when the whole app is closing.
-            if (rootActivity == null) {
-                leafTask = null;
-                break;
-            }
-            // We don't want the organizer to handle transition of non-embedded activity of other
-            // app.
-            if (r.getUid() != task.effectiveUid && !r.isEmbedded()) {
-                leafTask = null;
-                break;
-            }
-            leafTask = task;
-        }
-        mTempTransitionWindows.clear();
-        return leafTask;
-    }
-
-    /**
-     * Finds the common {@link android.window.TaskFragmentOrganizer} that organizes all embedded
-     * {@link TaskFragment} belong to the given {@link Task}.
-     * @return {@code null} if there is no such organizer, or if there are more than one.
-     */
-    @Nullable
-    private ITaskFragmentOrganizer findTaskFragmentOrganizer(@Nullable Task task) {
-        if (task == null) {
-            return null;
-        }
-        // We don't support remote animation for Task with multiple TaskFragmentOrganizers.
-        final ITaskFragmentOrganizer[] organizer = new ITaskFragmentOrganizer[1];
-        final boolean hasMultipleOrganizers = task.forAllLeafTaskFragments(taskFragment -> {
-            final ITaskFragmentOrganizer tfOrganizer = taskFragment.getTaskFragmentOrganizer();
-            if (tfOrganizer == null) {
-                return false;
-            }
-            if (organizer[0] != null && !organizer[0].asBinder().equals(tfOrganizer.asBinder())) {
-                return true;
-            }
-            organizer[0] = tfOrganizer;
-            return false;
-        });
-        if (hasMultipleOrganizers) {
-            ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for"
-                    + " Task with multiple TaskFragmentOrganizers.");
-            return null;
-        }
-        return organizer[0];
-    }
-
-    /**
-     * Overrides the pending transition with the remote animation defined by the
-     * {@link ITaskFragmentOrganizer} if all windows in the transition are children of
-     * {@link TaskFragment} that are organized by the same organizer.
-     *
-     * @return {@code true} if the transition is overridden.
-     */
-    private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit,
-            ArraySet<Integer> activityTypes) {
-        if (transitionMayContainNonAppWindows(transit)) {
-            return false;
-        }
-        if (!transitionContainsTaskFragmentWithBoundsOverride()) {
-            // No need to play TaskFragment remote animation if all embedded TaskFragment in the
-            // transition fill the Task.
-            return false;
-        }
-
-        final Task task = findParentTaskForAllEmbeddedWindows();
-        final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizer(task);
-        final RemoteAnimationDefinition definition = organizer != null
-                ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController
-                    .getRemoteAnimationDefinition(organizer)
-                : null;
-        final RemoteAnimationAdapter adapter = definition != null
-                ? definition.getAdapter(transit, activityTypes)
-                : null;
-        if (adapter == null) {
-            return false;
-        }
-        mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(
-                adapter, false /* sync */, true /*isActivityEmbedding*/);
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                "Override with TaskFragment remote animation for transit=%s",
-                AppTransition.appTransitionOldToString(transit));
-
-        final int organizerUid = mDisplayContent.mAtmService.mTaskFragmentOrganizerController
-                .getTaskFragmentOrganizerUid(organizer);
-        final boolean shouldDisableInputForRemoteAnimation = !task.isFullyTrustedEmbedding(
-                organizerUid);
-        final RemoteAnimationController remoteAnimationController =
-                mDisplayContent.mAppTransition.getRemoteAnimationController();
-        if (shouldDisableInputForRemoteAnimation && remoteAnimationController != null) {
-            // We are going to use client-driven animation, Disable all input on activity windows
-            // during the animation (unless it is fully trusted) to ensure it is safe to allow
-            // client to animate the surfaces.
-            // This is needed for all activity windows in the animation Task.
-            remoteAnimationController.setOnRemoteAnimationReady(() -> {
-                final Consumer<ActivityRecord> updateActivities =
-                        activity -> activity.setDropInputForAnimation(true);
-                task.forAllActivities(updateActivities);
-            });
-            ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, "Task=%d contains embedded TaskFragment."
-                    + " Disabled all input during TaskFragment remote animation.", task.mTaskId);
-        }
-        return true;
-    }
-
-    /**
-     * Overrides the pending transition with the remote animation defined for the transition in the
-     * set of defined remote animations in the app window token.
-     */
-    private void overrideWithRemoteAnimationIfSet(@Nullable ActivityRecord animLpActivity,
-            @TransitionOldType int transit, ArraySet<Integer> activityTypes) {
-        RemoteAnimationAdapter adapter = null;
-        if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) {
-            // The crash transition has higher priority than any involved remote animations.
-        } else if (AppTransition.isKeyguardGoingAwayTransitOld(transit)) {
-            adapter = mRemoteAnimationDefinition != null
-                    ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes)
-                    : null;
-        } else if (mDisplayContent.mAppTransition.getRemoteAnimationController() == null) {
-            adapter = getRemoteAnimationOverride(animLpActivity, transit, activityTypes);
-        }
-        if (adapter != null) {
-            mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
-        }
-    }
-
-    @Nullable
-    static Task findRootTaskFromContainer(WindowContainer wc) {
-        return wc.asTaskFragment() != null ? wc.asTaskFragment().getRootTask()
-                : wc.asActivityRecord().getRootTask();
-    }
-
-    @Nullable
-    static ActivityRecord getAppFromContainer(WindowContainer wc) {
-        return wc.asTaskFragment() != null ? wc.asTaskFragment().getTopNonFinishingActivity()
-                : wc.asActivityRecord();
-    }
-
-    /**
-     * @return The window token that determines the animation theme.
-     */
-    @Nullable
-    private ActivityRecord findAnimLayoutParamsToken(@TransitionOldType int transit,
-            ArraySet<Integer> activityTypes, ArraySet<ActivityRecord> openingApps,
-            ArraySet<ActivityRecord> closingApps, ArraySet<WindowContainer> changingApps) {
-        ActivityRecord result;
-
-        // Remote animations always win, but fullscreen tokens override non-fullscreen tokens.
-        result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
-                w -> w.getRemoteAnimationDefinition() != null
-                        && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes));
-        if (result != null) {
-            return result;
-        }
-        result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
-                w -> w.fillsParent() && w.findMainWindow() != null);
-        if (result != null) {
-            return result;
-        }
-        return lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
-                w -> w.findMainWindow() != null);
-    }
-
-    /**
-     * @return The set of {@link android.app.WindowConfiguration.ActivityType}s contained in the set
-     *         of apps in {@code array1}, {@code array2}, and {@code array3}.
-     */
-    private static ArraySet<Integer> collectActivityTypes(ArraySet<ActivityRecord> array1,
-            ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3) {
-        final ArraySet<Integer> result = new ArraySet<>();
-        for (int i = array1.size() - 1; i >= 0; i--) {
-            result.add(array1.valueAt(i).getActivityType());
-        }
-        for (int i = array2.size() - 1; i >= 0; i--) {
-            result.add(array2.valueAt(i).getActivityType());
-        }
-        for (int i = array3.size() - 1; i >= 0; i--) {
-            result.add(array3.valueAt(i).getActivityType());
-        }
-        return result;
-    }
-
-    private static ActivityRecord lookForHighestTokenWithFilter(ArraySet<ActivityRecord> array1,
-            ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3,
-            Predicate<ActivityRecord> filter) {
-        final int array2base = array1.size();
-        final int array3base = array2.size() + array2base;
-        final int count = array3base + array3.size();
-        int bestPrefixOrderIndex = Integer.MIN_VALUE;
-        ActivityRecord bestToken = null;
-        for (int i = 0; i < count; i++) {
-            final WindowContainer wtoken = i < array2base
-                    ? array1.valueAt(i)
-                    : (i < array3base
-                            ? array2.valueAt(i - array2base)
-                            : array3.valueAt(i - array3base));
-            final int prefixOrderIndex = wtoken.getPrefixOrderIndex();
-            final ActivityRecord r = getAppFromContainer(wtoken);
-            if (r != null && filter.test(r) && prefixOrderIndex > bestPrefixOrderIndex) {
-                bestPrefixOrderIndex = prefixOrderIndex;
-                bestToken = r;
-            }
-        }
-        return bestToken;
-    }
-
-    private boolean containsVoiceInteraction(ArraySet<ActivityRecord> apps) {
-        for (int i = apps.size() - 1; i >= 0; i--) {
-            if (apps.valueAt(i).mVoiceInteraction) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Apply animation to the set of window containers.
-     *
-     * @param wcs The list of {@link WindowContainer}s to which an app transition animation applies.
-     * @param apps The list of {@link ActivityRecord}s being transitioning.
-     * @param transit The current transition type.
-     * @param visible {@code true} if the apps becomes visible, {@code false} if the apps becomes
-     *                invisible.
-     * @param animLp Layout parameters in which an app transition animation runs.
-     * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice
-     *                         interaction session driving task.
-     */
-    private void applyAnimations(ArraySet<WindowContainer> wcs, ArraySet<ActivityRecord> apps,
-            @TransitionOldType int transit, boolean visible, LayoutParams animLp,
-            boolean voiceInteraction) {
-        final int wcsCount = wcs.size();
-        for (int i = 0; i < wcsCount; i++) {
-            final WindowContainer wc = wcs.valueAt(i);
-            // If app transition animation target is promoted to higher level, SurfaceAnimator
-            // triggers WC#onAnimationFinished only on the promoted target. So we need to take care
-            // of triggering AR#onAnimationFinished on each ActivityRecord which is a part of the
-            // app transition.
-            final ArrayList<ActivityRecord> transitioningDescendants = new ArrayList<>();
-            for (int j = 0; j < apps.size(); ++j) {
-                final ActivityRecord app = apps.valueAt(j);
-                if (app.isDescendantOf(wc)) {
-                    transitioningDescendants.add(app);
-                }
-            }
-            wc.applyAnimation(animLp, transit, visible, voiceInteraction, transitioningDescendants);
-        }
-    }
-
-    /**
-     * Returns {@code true} if a given {@link WindowContainer} is an embedded Task in
-     * {@link TaskView}.
-     *
-     * Note that this is a short term workaround to support Android Auto until it migrate to
-     * ShellTransition. This should only be used by {@link #getAnimationTargets}.
-     *
-     * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled.
-     */
-    static boolean isTaskViewTask(WindowContainer wc) {
-        // Use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and
-        // it is not guaranteed to work this logic in the future version.
-        boolean isTaskViewTask =  wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer;
-        if (isTaskViewTask) {
-            return true;
-        }
-
-        WindowContainer parent = wc.getParent();
-        boolean isParentATaskViewTask = parent != null
-                && parent instanceof Task
-                && ((Task) parent).mRemoveWithTaskOrganizer;
-        return isParentATaskViewTask;
-    }
-
-    /**
-     * Find WindowContainers to be animated from a set of opening and closing apps. We will promote
-     * animation targets to higher level in the window hierarchy if possible.
-     *
-     * @param visible {@code true} to get animation targets for opening apps, {@code false} to get
-     *                            animation targets for closing apps.
-     * @return {@link WindowContainer}s to be animated.
-     */
-    @VisibleForTesting
-    static ArraySet<WindowContainer> getAnimationTargets(
-            ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
-            boolean visible) {
-
-        // The candidates of animation targets, which might be able to promote to higher level.
-        final ArrayDeque<WindowContainer> candidates = new ArrayDeque<>();
-        final ArraySet<ActivityRecord> apps = visible ? openingApps : closingApps;
-        for (int i = 0; i < apps.size(); ++i) {
-            final ActivityRecord app = apps.valueAt(i);
-            if (app.shouldApplyAnimation(visible)) {
-                candidates.add(app);
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                        "Changing app %s visible=%b performLayout=%b",
-                        app, app.isVisible(), false);
-            }
-        }
-
-        final ArraySet<ActivityRecord> otherApps = visible ? closingApps : openingApps;
-        // Ancestors of closing apps while finding animation targets for opening apps, or ancestors
-        // of opening apps while finding animation targets for closing apps.
-        final ArraySet<WindowContainer> otherAncestors = new ArraySet<>();
-        for (int i = 0; i < otherApps.size(); ++i) {
-            for (WindowContainer wc = otherApps.valueAt(i); wc != null; wc = wc.getParent()) {
-                otherAncestors.add(wc);
-            }
-        }
-
-        // The final animation targets which cannot promote to higher level anymore.
-        final ArraySet<WindowContainer> targets = new ArraySet<>();
-        final ArrayList<WindowContainer> siblings = new ArrayList<>();
-        while (!candidates.isEmpty()) {
-            final WindowContainer current = candidates.removeFirst();
-            final WindowContainer parent = current.getParent();
-            siblings.clear();
-            siblings.add(current);
-            boolean canPromote = true;
-
-            if (isTaskViewTask(current)) {
-                // Don't animate an embedded Task in app transition. This is a short term workaround
-                // to prevent conflict of surface hierarchy changes between legacy app transition
-                // and TaskView (b/205189147).
-                // TODO(b/213312721): Remove this once ShellTransition is enabled.
-                continue;
-            } else if (parent == null || !parent.canCreateRemoteAnimationTarget()
-                    // We cannot promote the animation on Task's parent when the task is in
-                    // clearing task in case the animating get stuck when performing the opening
-                    // task that behind it.
-                    || (current.asTask() != null && current.asTask().mInRemoveTask)
-                    // We cannot promote the animation to changing window. This may happen when an
-                    // activity is open in a TaskFragment that is resizing, while the existing
-                    // activity in the TaskFragment is reparented to another TaskFragment.
-                    || parent.isChangingAppTransition()) {
-                canPromote = false;
-            } else {
-                // In case a descendant of the parent belongs to the other group, we cannot promote
-                // the animation target from "current" to the parent.
-                //
-                // Example: Imagine we're checking if we can animate a Task instead of a set of
-                // ActivityRecords. In case an activity starts a new activity within a same Task,
-                // an ActivityRecord of an existing activity belongs to the opening apps, at the
-                // same time, the other ActivityRecord of a new activity belongs to the closing
-                // apps. In this case, we cannot promote the animation target to Task level, but
-                // need to animate each individual activity.
-                //
-                // [Task] +- [ActivityRecord1] (in opening apps)
-                //        +- [ActivityRecord2] (in closing apps)
-                if (otherAncestors.contains(parent)) {
-                    canPromote = false;
-                }
-
-                // If the current window container is a task with adjacent task set, the both
-                // adjacent tasks will be opened or closed together. To get their opening or
-                // closing animation target independently, skip promoting their animation targets.
-                if (current.asTask() != null && current.asTask().hasAdjacentTask()) {
-                    canPromote = false;
-                }
-
-                // Find all siblings of the current WindowContainer in "candidates", move them into
-                // a separate list "siblings", and checks if an animation target can be promoted
-                // to its parent.
-                //
-                // We can promote an animation target to its parent if and only if all visible
-                // siblings will be animating.
-                //
-                // Example: Imagine that a Task contains two visible activity record, but only one
-                // of them is included in the opening apps and the other belongs to neither opening
-                // or closing apps. This happens when an activity launches another translucent
-                // activity in the same Task. In this case, we cannot animate Task, but have to
-                // animate each activity, otherwise an activity behind the translucent activity also
-                // animates.
-                //
-                // [Task] +- [ActivityRecord1] (visible, in opening apps)
-                //        +- [ActivityRecord2] (visible, not in opening apps)
-                for (int j = 0; j < parent.getChildCount(); ++j) {
-                    final WindowContainer sibling = parent.getChildAt(j);
-                    if (candidates.remove(sibling)) {
-                        if (!isTaskViewTask(sibling)) {
-                            // Don't animate an embedded Task in app transition. This is a short
-                            // term workaround to prevent conflict of surface hierarchy changes
-                            // between legacy app transition and TaskView (b/205189147).
-                            // TODO(b/213312721): Remove this once ShellTransition is enabled.
-                            siblings.add(sibling);
-                        }
-                    } else if (sibling != current && sibling.isVisible()) {
-                        canPromote = false;
-                    }
-                }
-            }
-
-            if (canPromote) {
-                candidates.add(parent);
-            } else {
-                targets.addAll(siblings);
-            }
-        }
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, "getAnimationTarget in=%s, out=%s",
-                apps, targets);
-        return targets;
-    }
-
-    /**
-     * Apply an app transition animation based on a set of {@link ActivityRecord}
-     *
-     * @param openingApps The list of opening apps to which an app transition animation applies.
-     * @param closingApps The list of closing apps to which an app transition animation applies.
-     * @param transit The current transition type.
-     * @param animLp Layout parameters in which an app transition animation runs.
-     * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice
-     *                         interaction session driving task.
-     */
-    private void applyAnimations(ArraySet<ActivityRecord> openingApps,
-            ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit,
-            LayoutParams animLp, boolean voiceInteraction) {
-        if (transit == WindowManager.TRANSIT_OLD_UNSET
-                || (openingApps.isEmpty() && closingApps.isEmpty())) {
-            return;
-        }
-
-        if (AppTransition.isActivityTransitOld(transit)) {
-            final ArrayList<Pair<ActivityRecord, Rect>> closingLetterboxes = new ArrayList();
-            for (int i = 0; i < closingApps.size(); ++i) {
-                ActivityRecord closingApp = closingApps.valueAt(i);
-                if (closingApp.areBoundsLetterboxed()) {
-                    final Rect insets = closingApp.getLetterboxInsets();
-                    closingLetterboxes.add(new Pair(closingApp, insets));
-                }
-            }
-
-            for (int i = 0; i < openingApps.size(); ++i) {
-                ActivityRecord openingApp = openingApps.valueAt(i);
-                if (openingApp.areBoundsLetterboxed()) {
-                    final Rect openingInsets = openingApp.getLetterboxInsets();
-                    for (Pair<ActivityRecord, Rect> closingLetterbox : closingLetterboxes) {
-                        final Rect closingInsets = closingLetterbox.second;
-                        if (openingInsets.equals(closingInsets)) {
-                            ActivityRecord closingApp = closingLetterbox.first;
-                            openingApp.setNeedsLetterboxedAnimation(true);
-                            closingApp.setNeedsLetterboxedAnimation(true);
-                        }
-                    }
-                }
-            }
-        }
-
-        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
-                openingApps, closingApps, true /* visible */);
-        final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
-                openingApps, closingApps, false /* visible */);
-        applyAnimations(openingWcs, openingApps, transit, true /* visible */, animLp,
-                voiceInteraction);
-        applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp,
-                voiceInteraction);
-
-        for (int i = 0; i < openingApps.size(); ++i) {
-            openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
-        }
-        for (int i = 0; i < closingApps.size(); ++i) {
-            closingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
-        }
-
-        final AccessibilityController accessibilityController =
-                mDisplayContent.mWmService.mAccessibilityController;
-        if (accessibilityController.hasCallbacks()) {
-            accessibilityController.onAppWindowTransition(mDisplayContent.getDisplayId(), transit);
-        }
-    }
-
-    private void handleOpeningApps() {
-        final ArraySet<ActivityRecord> openingApps = mDisplayContent.mOpeningApps;
-        final int appsCount = openingApps.size();
-
-        for (int i = 0; i < appsCount; i++) {
-            final ActivityRecord app = openingApps.valueAt(i);
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now opening app %s", app);
-
-            app.commitVisibility(true /* visible */, false /* performLayout */);
-
-            // In case a trampoline activity is used, it can happen that a new ActivityRecord is
-            // added and a new app transition starts before the previous app transition animation
-            // ends. So we cannot simply use app.isAnimating(PARENTS) to determine if the app must
-            // to be added to the list of tokens to be notified of app transition complete.
-            final WindowContainer wc = app.getAnimatingContainer(PARENTS,
-                    ANIMATION_TYPE_APP_TRANSITION);
-            if (wc == null || !wc.getAnimationSources().contains(app)) {
-                // This token isn't going to be animating. Add it to the list of tokens to
-                // be notified of app transition complete since the notification will not be
-                // sent be the app window animator.
-                mDisplayContent.mNoAnimationNotifyOnTransitionFinished.add(app.token);
-            }
-            app.updateReportedVisibilityLocked();
-            app.showAllWindowsLocked();
-
-            if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) {
-                app.attachThumbnailAnimation();
-            } else if (mDisplayContent.mAppTransition.isNextAppTransitionOpenCrossProfileApps()) {
-                app.attachCrossProfileAppsThumbnailAnimation();
-            }
-        }
-    }
-
-    private void handleClosingApps() {
-        final ArraySet<ActivityRecord> closingApps = mDisplayContent.mClosingApps;
-        final int appsCount = closingApps.size();
-
-        for (int i = 0; i < appsCount; i++) {
-            final ActivityRecord app = closingApps.valueAt(i);
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now closing app %s", app);
-
-            app.commitVisibility(false /* visible */, false /* performLayout */);
-            app.updateReportedVisibilityLocked();
-            // Force the allDrawn flag, because we want to start
-            // this guy's animations regardless of whether it's
-            // gotten drawn.
-            app.allDrawn = true;
-            // Ensure that apps that are mid-starting are also scheduled to have their
-            // starting windows removed after the animation is complete
-            if (app.mStartingWindow != null && !app.mStartingWindow.mAnimatingExit) {
-                app.removeStartingWindow();
-            }
-
-            if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailDown()) {
-                app.attachThumbnailAnimation();
-            }
-        }
-    }
-
-    private void handleClosingChangingContainers() {
-        final ArrayMap<WindowContainer, Rect> containers =
-                mDisplayContent.mClosingChangingContainers;
-        while (!containers.isEmpty()) {
-            final WindowContainer container = containers.keyAt(0);
-            containers.remove(container);
-
-            // For closing changing windows that are part of the transition, they should have been
-            // removed from mClosingChangingContainers in WindowContainer#getAnimationAdapter()
-            // If the closing changing TaskFragment is not part of the transition, update its
-            // surface after removing it from mClosingChangingContainers.
-            final TaskFragment taskFragment = container.asTaskFragment();
-            if (taskFragment != null) {
-                taskFragment.updateOrganizedTaskFragmentSurface();
-            }
-        }
-    }
-
-    private void handleChangingApps(@TransitionOldType int transit) {
-        final ArraySet<WindowContainer> apps = mDisplayContent.mChangingContainers;
-        final int appsCount = apps.size();
-        for (int i = 0; i < appsCount; i++) {
-            WindowContainer wc = apps.valueAt(i);
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now changing app %s", wc);
-            wc.applyAnimation(null, transit, true, false, null /* sources */);
-        }
-    }
-
-    private boolean transitionGoodToGo(ArraySet<? extends WindowContainer> apps,
-            ArrayMap<WindowContainer, Integer> outReasons) {
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                "Checking %d opening apps (timeout=%b)...", apps.size(),
-                mDisplayContent.mAppTransition.isTimeout());
-        if (mDisplayContent.mAppTransition.isTimeout()) {
-            return true;
-        }
-
-        for (int i = 0; i < apps.size(); i++) {
-            WindowContainer wc = apps.valueAt(i);
-            final ActivityRecord activity = getAppFromContainer(wc);
-            if (activity == null) {
-                continue;
-            }
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                    "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
-                            + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
-                    activity, activity.allDrawn, activity.isStartingWindowDisplayed(),
-                    activity.startingMoved, activity.isRelaunching(),
-                    activity.mStartingWindow);
-            final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
-            if (!allDrawn && !activity.isStartingWindowDisplayed() && !activity.startingMoved) {
-                return false;
-            }
-            if (allDrawn) {
-                outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
-            } else {
-                outReasons.put(activity,
-                        activity.mStartingData instanceof SplashScreenStartingData
-                                ? APP_TRANSITION_SPLASH_SCREEN
-                                : APP_TRANSITION_SNAPSHOT);
-            }
-        }
-
-        // We also need to wait for the specs to be fetched, if needed.
-        if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
-            return false;
-        }
-
-        if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
-            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
-                    mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
-            return false;
-        }
-
-        // If the wallpaper is visible, we need to check it's ready too.
-        return !mWallpaperControllerLocked.isWallpaperVisible()
-                || mWallpaperControllerLocked.wallpaperTransitionReady();
-    }
-
-    private boolean transitionGoodToGoForTaskFragments() {
-        if (mDisplayContent.mAppTransition.isTimeout()) {
-            return true;
-        }
-
-        // Check all Tasks in this transition. This is needed because new TaskFragment created for
-        // launching activity may not be in the tracking lists, but we still want to wait for the
-        // activity launch to start the transition.
-        final ArraySet<Task> rootTasks = new ArraySet<>();
-        for (int i = mDisplayContent.mOpeningApps.size() - 1; i >= 0; i--) {
-            rootTasks.add(mDisplayContent.mOpeningApps.valueAt(i).getRootTask());
-        }
-        for (int i = mDisplayContent.mClosingApps.size() - 1; i >= 0; i--) {
-            rootTasks.add(mDisplayContent.mClosingApps.valueAt(i).getRootTask());
-        }
-        for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
-            rootTasks.add(
-                    findRootTaskFromContainer(mDisplayContent.mChangingContainers.valueAt(i)));
-        }
-
-        // Organized TaskFragment can be empty for two situations:
-        // 1. New created and is waiting for Activity launch. In this case, we want to wait for
-        //    the Activity launch to trigger the transition.
-        // 2. Last Activity is just removed. In this case, we want to wait for organizer to
-        //    remove the TaskFragment because it may also want to change other TaskFragments in
-        //    the same transition.
-        for (int i = rootTasks.size() - 1; i >= 0; i--) {
-            final Task rootTask = rootTasks.valueAt(i);
-            if (rootTask == null) {
-                // It is possible that one activity may have been removed from the hierarchy. No
-                // need to check for this case.
-                continue;
-            }
-            final boolean notReady = rootTask.forAllLeafTaskFragments(taskFragment -> {
-                if (!taskFragment.isReadyToTransit()) {
-                    ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Organized TaskFragment is not ready= %s",
-                            taskFragment);
-                    return true;
-                }
-                return false;
-            });
-            if (notReady) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Identifies whether the current transition occurs within a single task or not. This is used
-     * to determine whether animations should be clipped to the task bounds instead of root task
-     * bounds.
-     */
-    @VisibleForTesting
-    boolean isTransitWithinTask(@TransitionOldType int transit, Task task) {
-        if (task == null
-                || !mDisplayContent.mChangingContainers.isEmpty()) {
-            // if there is no task, then we can't constrain to the task.
-            // if anything is changing, it can animate outside its task.
-            return false;
-        }
-        if (!(transit == TRANSIT_OLD_ACTIVITY_OPEN
-                || transit == TRANSIT_OLD_ACTIVITY_CLOSE
-                || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH)) {
-            // only activity-level transitions will be within-task.
-            return false;
-        }
-        // check that all components are in the task.
-        for (ActivityRecord activity : mDisplayContent.mOpeningApps) {
-            Task activityTask = activity.getTask();
-            if (activityTask != task) {
-                return false;
-            }
-        }
-        for (ActivityRecord activity : mDisplayContent.mClosingApps) {
-            if (activity.getTask() != task) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static boolean canBeWallpaperTarget(ArraySet<ActivityRecord> apps) {
-        for (int i = apps.size() - 1; i >= 0; i--) {
-            if (apps.valueAt(i).windowsCanBeWallpaperTarget()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Finds the top app in a list of apps, using its {@link ActivityRecord#getPrefixOrderIndex} to
-     * compare z-order.
-     *
-     * @param apps The list of apps to search.
-     * @param ignoreInvisible If set to true, ignores apps that are not
-     *                        {@link ActivityRecord#isVisible}.
-     * @return The top {@link ActivityRecord}.
-     */
-    private static ActivityRecord getTopApp(ArraySet<? extends WindowContainer> apps,
-            boolean ignoreInvisible) {
-        int topPrefixOrderIndex = Integer.MIN_VALUE;
-        ActivityRecord topApp = null;
-        for (int i = apps.size() - 1; i >= 0; i--) {
-            final ActivityRecord app = getAppFromContainer(apps.valueAt(i));
-            if (app == null || ignoreInvisible && !app.isVisible()) {
-                continue;
-            }
-            final int prefixOrderIndex = app.getPrefixOrderIndex();
-            if (prefixOrderIndex > topPrefixOrderIndex) {
-                topPrefixOrderIndex = prefixOrderIndex;
-                topApp = app;
-            }
-        }
-        return topApp;
-    }
-}
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 576e5d5..439b503 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -506,6 +506,10 @@
             context =  new ContextThemeWrapper(context, context.getThemeResId()) {
                 @Override
                 public void startActivity(Intent intent) {
+                    // PageSizeMismatch dialog stays on top of the browser even after opening link
+                    // set broadcast to close the dialog when link has been clicked.
+                    sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                     super.startActivity(intent);
                 }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 79bed3d..e76a834 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -388,8 +388,7 @@
                                 removedWindowContainer);
                 mBackAnimationInProgress = builder != null;
                 if (mBackAnimationInProgress) {
-                    if (removedWindowContainer.mTransitionController.inTransition()
-                            || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) {
+                    if (removedWindowContainer.mTransitionController.inTransition()) {
                         ProtoLog.w(WM_DEBUG_BACK_PREVIEW,
                                 "Pending back animation due to another animation is running");
                         mPendingAnimationBuilder = builder;
@@ -817,6 +816,8 @@
         if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */)
                 && ar.mTransitionController.isCollecting(ar)) {
             final TransitionController controller = ar.mTransitionController;
+            final Transition transition = controller.getCollectingTransition();
+            final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType;
             boolean collectTask = false;
             ActivityRecord changedActivity = null;
             for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) {
@@ -829,8 +830,16 @@
                     changedActivity = next;
                 }
             }
-            if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType
-                    == AnimationHandler.TASK_SWITCH) {
+            if (Flags.unifyBackNavigationTransition()) {
+                for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) {
+                    collectAnimatableTarget(transition, switchType,
+                            mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget,
+                            false /* isTop */);
+                }
+                collectAnimatableTarget(transition, switchType,
+                        mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */);
+            }
+            if (collectTask && switchType == AnimationHandler.TASK_SWITCH) {
                 final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask();
                 if (topTask != null) {
                     WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent();
@@ -848,6 +857,18 @@
         }
     }
 
+    private static void collectAnimatableTarget(Transition transition, int switchType,
+            WindowContainer animatingTarget, boolean isTop) {
+        if ((switchType == AnimationHandler.ACTIVITY_SWITCH
+                && (animatingTarget.asActivityRecord() != null
+                        || animatingTarget.asTaskFragment() != null))
+                || (switchType == AnimationHandler.TASK_SWITCH
+                        && animatingTarget.asTask() != null)) {
+            transition.collect(animatingTarget);
+            transition.setBackGestureAnimation(animatingTarget, isTop);
+        }
+    }
+
     // For shell transition
     /**
      * Check whether the transition targets was animated by back gesture animation.
@@ -992,8 +1013,8 @@
             return;
         }
 
-        if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) {
-            Slog.v(TAG, "Skip predictive back transition, another transition is collecting");
+        if (mWindowManagerService.mRoot.mTransitionController.inTransition()) {
+            Slog.v(TAG, "Skip predictive back transition, another transition is playing");
             cancelPendingAnimation();
             return;
         }
@@ -1098,7 +1119,7 @@
             }
 
             final Transition prepareTransition = builder.prepareTransitionIfNeeded(
-                    openingActivities);
+                    openingActivities, close, open);
             final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction();
             final SurfaceControl.Transaction ct = prepareTransition != null
                     ? st : close.getPendingTransaction();
@@ -1790,7 +1811,8 @@
                 return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget);
             }
 
-            private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) {
+            private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities,
+                    WindowContainer promoteToClose, WindowContainer[] promoteToOpen) {
                 if (Flags.unifyBackNavigationTransition()) {
                     if (mCloseTarget.asWindowState() != null) {
                         return null;
@@ -1806,11 +1828,11 @@
                     final TransitionController tc = visibleOpenActivities[0].mTransitionController;
                     final Transition prepareOpen = tc.createTransition(
                             TRANSIT_PREPARE_BACK_NAVIGATION);
-                    tc.collect(mCloseTarget);
-                    prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */);
-                    for (int i = mOpenTargets.length - 1; i >= 0; --i) {
-                        tc.collect(mOpenTargets[i]);
-                        prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */);
+                    tc.collect(promoteToClose);
+                    prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */);
+                    for (int i = promoteToOpen.length - 1; i >= 0; --i) {
+                        tc.collect(promoteToOpen[i]);
+                        prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */);
                     }
                     if (!makeVisibles.isEmpty()) {
                         setLaunchBehind(visibleOpenActivities);
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index a1faa75..f359307 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -51,8 +51,13 @@
     }
 
     /**
-     * Return {@code true} if the current device supports desktop mode.
+     * Return {@code true} if the current device can hosts desktop sessions on its internal display.
      */
+    @VisibleForTesting
+    static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+        return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
+    }
+
     // TODO(b/337819319): use a companion object instead.
     private static boolean isDesktopModeSupported(@NonNull Context context) {
         return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
@@ -67,12 +72,12 @@
      */
     private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) {
         return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported(
-                context) || isDeviceEligibleForDesktopMode(context));
+                context) || isInternalDisplayEligibleToHostDesktops(context));
     }
 
     @VisibleForTesting
-    static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
-        return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context)  || (
+    static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
+        return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
                 Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported(
                         context));
     }
@@ -81,12 +86,14 @@
      * Return {@code true} if desktop mode can be entered on the current device.
      */
     static boolean canEnterDesktopMode(@NonNull Context context) {
-        return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context))
+        return (isInternalDisplayEligibleToHostDesktops(context)
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
+                && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions()))
                 || isDesktopModeEnabledByDevOption(context);
     }
 
     /** Returns {@code true} if desktop experience wallpaper is supported on this device. */
     public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) {
-        return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context);
+        return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context);
     }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 1dd7c4d..682f3d8 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -226,7 +226,6 @@
 import android.view.InsetsState;
 import android.view.MagnificationSpec;
 import android.view.PrivacyIndicatorBounds;
-import android.view.RemoteAnimationDefinition;
 import android.view.RoundedCorners;
 import android.view.Surface;
 import android.view.Surface.Rotation;
@@ -367,8 +366,6 @@
     private int mMaxUiWidth = 0;
 
     final AppTransition mAppTransition;
-    final AppTransitionController mAppTransitionController;
-    boolean mSkipAppTransitionAnimation = false;
 
     final ArraySet<ActivityRecord> mOpeningApps = new ArraySet<>();
     final ArraySet<ActivityRecord> mClosingApps = new ArraySet<>();
@@ -547,9 +544,6 @@
     // TODO(multi-display): remove some of the usages.
     boolean isDefaultDisplay;
 
-    /** Indicates whether any presentation is shown on this display. */
-    boolean mIsPresenting;
-
     /** Save allocating when calculating rects */
     private final Rect mTmpRect = new Rect();
     private final Region mTmpRegion = new Region();
@@ -1164,7 +1158,6 @@
         mAppTransition = new AppTransition(mWmService.mContext, mWmService, this);
         mAppTransition.registerListenerLocked(mWmService.mActivityManagerAppTransitionNotifier);
         mAppTransition.registerListenerLocked(mFixedRotationTransitionListener);
-        mAppTransitionController = new AppTransitionController(mWmService, this);
         mTransitionController.registerLegacyListener(mFixedRotationTransitionListener);
         mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
         mRemoteDisplayChangeController = new RemoteDisplayChangeController(this);
@@ -1556,10 +1549,6 @@
         return mInputMethodSurfaceParentWindow;
     }
 
-    void registerRemoteAnimations(RemoteAnimationDefinition definition) {
-        mAppTransitionController.registerRemoteAnimations(definition);
-    }
-
     void reconfigureDisplayLocked() {
         if (!isReady()) {
             return;
@@ -4661,35 +4650,6 @@
         }
     }
 
-    /**
-     * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to
-     * judge whether or not to notify the IME insets provider to dispatch this reported IME client
-     * visibility state to the app clients when needed.
-     */
-    boolean onImeInsetsClientVisibilityUpdate() {
-        boolean[] changed = new boolean[1];
-
-        // Unlike the IME layering target or the control target can be updated during the layout
-        // change, the IME input target requires to be changed after gaining the input focus.
-        // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze
-        // when activities going to be visible until the input target changed, or the
-        // activity was the current input target that has to unfreeze after updating the IME
-        // client visibility.
-        final ActivityRecord inputTargetActivity =
-                mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null;
-        final boolean targetChanged = mImeInputTarget != mLastImeInputTarget;
-        if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested()
-                && inputTargetActivity.mImeInsetsFrozenUntilStartInput) {
-            forAllActivities(r -> {
-                if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) {
-                    r.mImeInsetsFrozenUntilStartInput = false;
-                    changed[0] = true;
-                }
-            });
-        }
-        return changed[0];
-    }
-
     void updateImeControlTarget() {
         updateImeControlTarget(false /* forceUpdateImeParent */);
     }
@@ -5636,20 +5596,6 @@
     }
 
     /**
-     * Transfer app transition from other display to this display.
-     *
-     * @param from Display from where the app transition is transferred.
-     *
-     * TODO(new-app-transition): Remove this once the shell handles app transition.
-     */
-    void transferAppTransitionFrom(DisplayContent from) {
-        final boolean prepared = mAppTransition.transferFrom(from.mAppTransition);
-        if (prepared && okToAnimate()) {
-            mSkipAppTransitionAnimation = false;
-        }
-    }
-
-    /**
      * @deprecated new transition should use {@link #requestTransitionAndLegacyPrepare(int, int)}
      */
     @Deprecated
@@ -5663,10 +5609,7 @@
     @Deprecated
     void prepareAppTransition(@WindowManager.TransitionType int transit,
             @WindowManager.TransitionFlags int flags) {
-        final boolean prepared = mAppTransition.prepareAppTransition(transit, flags);
-        if (prepared && okToAnimate() && transit != TRANSIT_NONE) {
-            mSkipAppTransitionAnimation = false;
-        }
+        mAppTransition.prepareAppTransition(transit, flags);
     }
 
     /**
@@ -7141,14 +7084,19 @@
         }
 
         /**
+         * @return an integer as the changed requested visible insets types.
          * @see #getRequestedVisibleTypes()
          */
-        void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
-            int newRequestedVisibleTypes =
+        @InsetsType int updateRequestedVisibleTypes(
+                @InsetsType int visibleTypes, @InsetsType int mask) {
+            final int newRequestedVisibleTypes =
                     (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
             if (mRequestedVisibleTypes != newRequestedVisibleTypes) {
+                final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes;
                 mRequestedVisibleTypes = newRequestedVisibleTypes;
+                return changedTypes;
             }
+            return 0;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 907d0dc..7b6fc9e 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -34,6 +34,7 @@
 import android.view.InputApplicationHandle;
 import android.view.InputChannel;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.window.InputTransferToken;
 
 import com.android.internal.protolog.ProtoLog;
@@ -260,7 +261,7 @@
 
         // The EmbeddedWindow can only request the IME. All other insets types are requested by
         // the host window.
-        private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0;
+        private @InsetsType int mRequestedVisibleTypes = 0;
 
         /** Whether the gesture is transferred to embedded window. */
         boolean mGestureToEmbedded = false;
@@ -354,24 +355,28 @@
         }
 
         @Override
-        public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) {
+        public boolean isRequestedVisible(@InsetsType int types) {
             return (mRequestedVisibleTypes & types) != 0;
         }
 
         @Override
-        public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() {
+        public @InsetsType int getRequestedVisibleTypes() {
             return mRequestedVisibleTypes;
         }
 
         /**
          * Only the IME can be requested from the EmbeddedWindow.
-         * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are
+         * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are
          *                              not sent to system server via WindowlessWindowManager.
+         * @return an integer as the changed requested visible insets types.
          */
-        void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) {
+        @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
             if (mRequestedVisibleTypes != requestedVisibleTypes) {
+                final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes;
                 mRequestedVisibleTypes = requestedVisibleTypes;
+                return changedTypes;
             }
+            return 0;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index cf16204..f52446f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -282,7 +282,14 @@
             // TODO(b/353463205) investigate if we should fail the statsToken, or if it's only
             //  temporary null.
             if (target != null) {
-                invokeOnImeRequestedChangedListener(target.getWindow(), statsToken);
+                // If insets target is not available (e.g. RemoteInsetsControlTarget), use current
+                // IME input target to update IME request state. For example, switch from a task
+                // with showing IME to a split-screen task without showing IME.
+                InsetsTarget insetsTarget = target.getWindow();
+                if (insetsTarget == null && mServerVisible) {
+                    insetsTarget = mDisplayContent.getImeInputTarget();
+                }
+                invokeOnImeRequestedChangedListener(insetsTarget, statsToken);
             }
         }
     }
@@ -314,7 +321,6 @@
                 reportImeDrawnForOrganizerIfNeeded((InsetsControlTarget) caller);
             }
         }
-        changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate();
         if (Flags.refactorInsetsController()) {
             if (changed) {
                 ImeTracker.forLogging().onProgress(statsToken,
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 4bcba13..b4d55a1 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -387,22 +387,6 @@
                 state.addSource(navSource);
             }
             return state;
-        } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) {
-            // During switching tasks with gestural navigation, before the next IME input target
-            // starts the input, we should adjust and freeze the last IME visibility of the window
-            // in case delivering obsoleted IME insets state during transitioning.
-            final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
-
-            if (originalImeSource != null) {
-                final boolean imeVisibility = w.isRequestedVisible(Type.ime());
-                final InsetsState state = copyState
-                        ? new InsetsState(originalState)
-                        : originalState;
-                final InsetsSource imeSource = new InsetsSource(originalImeSource);
-                imeSource.setVisible(imeVisibility);
-                state.addSource(imeSource);
-                return state;
-            }
         } else if (w.mImeInsetsConsumed) {
             // Set the IME source (if there is one) to be invisible if it has been consumed.
             final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
@@ -453,9 +437,9 @@
         return originalState;
     }
 
-    void onRequestedVisibleTypesChanged(InsetsTarget caller,
+    void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes,
             @Nullable ImeTracker.Token statsToken) {
-        mStateController.onRequestedVisibleTypesChanged(caller, statsToken);
+        mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken);
         checkAbortTransient(caller);
         updateBarControlTarget(mFocusedWin);
     }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 9202cf2..164abab 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -219,14 +219,20 @@
         }
     }
 
-    void onRequestedVisibleTypesChanged(InsetsTarget caller,
+    void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes,
             @Nullable ImeTracker.Token statsToken) {
         boolean changed = false;
         for (int i = mProviders.size() - 1; i >= 0; i--) {
             final InsetsSourceProvider provider = mProviders.valueAt(i);
-            final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime();
-            changed |= provider.updateClientVisibility(caller,
-                    isImeProvider ? statsToken : null);
+            final @InsetsType int type = provider.getSource().getType();
+            if ((type & changedTypes) != 0) {
+                final boolean isImeProvider = type == WindowInsets.Type.ime();
+                changed |= provider.updateClientVisibility(
+                                caller, isImeProvider ? statsToken : null)
+                        // Fake control target cannot change the client visibility, but it should
+                        // change the insets with its newly requested visibility.
+                        || (caller == provider.getFakeControlTarget());
+            }
         }
         if (changed) {
             notifyInsetsChanged();
@@ -435,7 +441,8 @@
             for (int i = newControlTargets.size() - 1; i >= 0; i--) {
                 // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the
                 //  IME provider. Check if we have to create a new request here
-                onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */);
+                onRequestedVisibleTypesChanged(newControlTargets.valueAt(i),
+                        WindowInsets.Type.all(), null /* statsToken */);
             }
             newControlTargets.clear();
             if (!android.view.inputmethod.Flags.refactorInsetsController()) {
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
new file mode 100644
index 0000000..6946343
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
+import android.annotation.NonNull;
+import android.util.IntArray;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.WmProtoLogGroups;
+
+/**
+ * Manages presentation windows.
+ */
+class PresentationController {
+
+    // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
+    private final IntArray mPresentingDisplayIds = new IntArray();
+
+    PresentationController() {}
+
+    private boolean isPresenting(int displayId) {
+        return mPresentingDisplayIds.contains(displayId);
+    }
+
+    boolean shouldOccludeActivities(int displayId) {
+        // All activities on the presenting display must be hidden so that malicious apps can't do
+        // tap jacking (b/391466268).
+        // For now, this should only be applied to external displays because presentations can only
+        // be shown on them.
+        // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
+        // the presentation won't stop its controlling activity.
+        return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+    }
+
+    void onPresentationAdded(@NonNull WindowState win) {
+        final int displayId = win.getDisplayId();
+        if (isPresenting(displayId)) {
+            return;
+        }
+        ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
+                win.getDisplayId(), win);
+        mPresentingDisplayIds.add(win.getDisplayId());
+        if (enablePresentationForConnectedDisplays()) {
+            // A presentation hides all activities behind on the same display.
+            win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
+                    /*notifyClients=*/ true);
+        }
+        win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
+    }
+
+    void onPresentationRemoved(@NonNull WindowState win) {
+        final int displayId = win.getDisplayId();
+        if (!isPresenting(displayId)) {
+            return;
+        }
+        ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
+                "Presentation removed from display %d: %s", win.getDisplayId(), win);
+        // TODO(b/393945496): Make sure that there's one presentation at most per display.
+        final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
+        if (displayIdIndex != -1) {
+            mPresentingDisplayIds.remove(displayIdIndex);
+        }
+        if (enablePresentationForConnectedDisplays()) {
+            // A presentation hides all activities behind on the same display.
+            win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
+                    /*notifyClients=*/ true);
+        }
+        win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 95d9b3e..c93efd3 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -35,7 +35,6 @@
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_SLEEP;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_WAKE;
 
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS_LIGHT;
@@ -68,7 +67,6 @@
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
 import static com.android.server.wm.ActivityTaskSupervisor.dumpHistoryList;
 import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity;
-import static com.android.server.wm.KeyguardController.KEYGUARD_SLEEP_TOKEN_TAG;
 import static com.android.server.wm.RootWindowContainerProto.IS_HOME_RECENTS_COMPONENT;
 import static com.android.server.wm.RootWindowContainerProto.KEYGUARD_CONTROLLER;
 import static com.android.server.wm.RootWindowContainerProto.WINDOW_CONTAINER;
@@ -803,8 +801,6 @@
         mWmService.mAtmService.mTaskFragmentOrganizerController.dispatchPendingEvents();
         mWmService.mSyncEngine.onSurfacePlacement();
 
-        checkAppTransitionReady(surfacePlacer);
-
         mWmService.mAtmService.mBackNavigationController
                 .checkAnimationReady(defaultDisplay.mWallpaperController);
 
@@ -898,38 +894,6 @@
         if (DEBUG_WINDOW_TRACE) Slog.e(TAG, "performSurfacePlacementInner exit");
     }
 
-    private void checkAppTransitionReady(WindowSurfacePlacer surfacePlacer) {
-        // Trace all displays app transition by Z-order for pending layout change.
-        for (int i = mChildren.size() - 1; i >= 0; --i) {
-            final DisplayContent curDisplay = mChildren.get(i);
-
-            // If we are ready to perform an app transition, check through all of the app tokens
-            // to be shown and see if they are ready to go.
-            if (curDisplay.mAppTransition.isReady()) {
-                // handleAppTransitionReady may modify curDisplay.pendingLayoutChanges.
-                curDisplay.mAppTransitionController.handleAppTransitionReady();
-                if (DEBUG_LAYOUT_REPEATS) {
-                    surfacePlacer.debugLayoutRepeats("after handleAppTransitionReady",
-                            curDisplay.pendingLayoutChanges);
-                }
-            }
-
-            if (curDisplay.mAppTransition.isRunning() && !curDisplay.isAppTransitioning()) {
-                // We have finished the animation of an app transition. To do this, we have
-                // delayed a lot of operations like showing and hiding apps, moving apps in
-                // Z-order, etc.
-                // The app token list reflects the correct Z-order, but the window list may now
-                // be out of sync with it. So here we will just rebuild the entire app window
-                // list. Fun!
-                curDisplay.handleAnimatingStoppedAndTransition();
-                if (DEBUG_LAYOUT_REPEATS) {
-                    surfacePlacer.debugLayoutRepeats("after handleAnimStopAndXitionLock",
-                            curDisplay.pendingLayoutChanges);
-                }
-            }
-        }
-    }
-
     private void applySurfaceChangesTransaction() {
         // TODO(multi-display): Support these features on secondary screens.
         final DisplayContent defaultDc = mDefaultDisplay;
@@ -2266,20 +2230,6 @@
 
                 // Ensure the leash of new task is in sync with its current bounds after reparent.
                 rootTask.maybeApplyLastRecentsAnimationTransaction();
-
-                // In the case of this activity entering PIP due to it being moved to the back,
-                // the old activity would have a TRANSIT_TASK_TO_BACK transition that needs to be
-                // ran. But, since its visibility did not change (note how it was STOPPED/not
-                // visible, and with it now at the back stack, it remains not visible), the logic to
-                // add the transition is automatically skipped. We then add this activity manually
-                // to the list of apps being closed, and request its transition to be ran.
-                final ActivityRecord oldTopActivity = task.getTopMostActivity();
-                if (oldTopActivity != null && oldTopActivity.isState(STOPPED)
-                        && task.getDisplayContent().mAppTransition.containsTransitRequest(
-                        TRANSIT_TO_BACK)) {
-                    task.getDisplayContent().mClosingApps.add(oldTopActivity);
-                    oldTopActivity.mRequestForceTransition = true;
-                }
             }
 
             // TODO(remove-legacy-transit): Move this to the `singleActivity` case when removing
@@ -2958,20 +2908,6 @@
         display.mAllSleepTokens.remove(token);
         if (display.mAllSleepTokens.isEmpty()) {
             mService.updateSleepIfNeededLocked();
-            // Assuming no lock screen is set and a user launches an activity, turns off the screen
-            // and turn on the screen again, then the launched activity should be displayed on the
-            // screen without app transition animation. When the screen turns on, both keyguard
-            // sleep token and display off sleep token are removed, but the order is
-            // non-deterministic.
-            // Note: Display#mSkipAppTransitionAnimation will be ignored when keyguard related
-            // transition exists, so this affects only when no lock screen is set. Otherwise
-            // keyguard going away animation will be played.
-            // See also AppTransitionController#getTransitCompatType for more details.
-            if ((!mTaskSupervisor.getKeyguardController().isKeyguardOccluded(display.mDisplayId)
-                    && token.mTag.equals(KEYGUARD_SLEEP_TOKEN_TAG))
-                    || token.mTag.equals(DISPLAY_OFF_SLEEP_TOKEN_TAG)) {
-                display.mSkipAppTransitionAnimation = true;
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 1ad5988..8d198b2 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -704,9 +704,10 @@
                     ImeTracker.forLogging().onProgress(imeStatsToken,
                             ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
                 }
-                win.setRequestedVisibleTypes(requestedVisibleTypes);
+                final @InsetsType int changedTypes =
+                        win.setRequestedVisibleTypes(requestedVisibleTypes);
                 win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win,
-                        imeStatsToken);
+                        changedTypes, imeStatsToken);
                 final Task task = win.getTask();
                 if (task != null) {
                     task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true);
@@ -723,10 +724,11 @@
                     // TODO(b/353463205) Use different phase here
                     ImeTracker.forLogging().onProgress(imeStatsToken,
                             ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
-                    embeddedWindow.setRequestedVisibleTypes(
+                    final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes(
                             requestedVisibleTypes & WindowInsets.Type.ime());
                     embeddedWindow.getDisplayContent().getInsetsPolicy()
-                            .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken);
+                            .onRequestedVisibleTypesChanged(
+                                    embeddedWindow, changedTypes, imeStatsToken);
                 } else {
                     ImeTracker.forLogging().onFailed(imeStatsToken,
                             ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java
index dcdffa4..2664dcd 100644
--- a/services/core/java/com/android/server/wm/SnapshotController.java
+++ b/services/core/java/com/android/server/wm/SnapshotController.java
@@ -72,11 +72,6 @@
         mActivitySnapshotController.notifyAppVisibilityChanged(appWindowToken, visible);
     }
 
-    // For legacy transition, which won't support activity snapshot
-    void onTransitionStarting(DisplayContent displayContent) {
-        mTaskSnapshotController.handleClosingApps(displayContent.mClosingApps);
-    }
-
     // For shell transition, record snapshots before transaction start.
     void onTransactionReady(@WindowManager.TransitionType int type,
             ArrayList<Transition.ChangeInfo> changeInfos) {
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 3dfff39..c5425fe 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -132,10 +132,7 @@
                         animationFinishCallback.onAnimationFinished(type, anim);
                     }
                 };
-                // If both the Animatable and AnimationAdapter requests to be deferred, only the
-                // first one will be called.
-                if (!(mAnimatable.shouldDeferAnimationFinish(resetAndInvokeFinish)
-                        || anim.shouldDeferAnimationFinish(resetAndInvokeFinish))) {
+                if (!anim.shouldDeferAnimationFinish(resetAndInvokeFinish)) {
                     resetAndInvokeFinish.run();
                 }
                 mAnimationFinished = true;
@@ -639,23 +636,5 @@
          * @return The height of the surface to be animated.
          */
         int getSurfaceHeight();
-
-        /**
-         * Gets called when the animation is about to finish and gives the client the opportunity to
-         * defer finishing the animation, i.e. it keeps the leash around until the client calls
-         * {@link #cancelAnimation}.
-         * <p>
-         * {@link AnimationAdapter} has a similar method which is called only if this method returns
-         * false. This mean that if both this {@link Animatable} and the {@link AnimationAdapter}
-         * request to be deferred, this method is the sole responsible to call
-         * endDeferFinishCallback. On the other hand, the animation finish might still be deferred
-         * if this method return false and the one from the {@link AnimationAdapter} returns true.
-         *
-         * @param endDeferFinishCallback The callback to call when defer finishing should be ended.
-         * @return Whether the client would like to defer the animation finish.
-         */
-        default boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
-            return false;
-        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index f75e717..3abab8b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -508,9 +508,6 @@
      */
     boolean mAllowForceResizeOverride = true;
 
-    private final AnimatingActivityRegistry mAnimatingActivityRegistry =
-            new AnimatingActivityRegistry();
-
     private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_TASK_MSG + 1;
 
     private final Handler mHandler;
@@ -1122,17 +1119,6 @@
         // already ran fully within super.onParentChanged
         updateTaskOrganizerState();
 
-        // TODO(b/168037178): The check for null display content and setting it to null doesn't
-        //                    really make sense here...
-
-        // TODO(b/168037178): This is mostly taking care of the case where the stask is removing
-        //                    from the display, so we should probably consolidate it there instead.
-
-        if (getParent() == null && mDisplayContent != null) {
-            mDisplayContent = null;
-            mWmService.mWindowPlacerLocked.requestTraversal();
-        }
-
         if (oldParent != null) {
             final Task oldParentTask = oldParent.asTask();
             if (oldParentTask != null) {
@@ -1185,9 +1171,6 @@
         }
 
         mRootWindowContainer.updateUIDsPresentOnDisplay();
-
-        // Ensure all animations are finished at same time in split-screen mode.
-        forAllActivities(ActivityRecord::updateAnimatingActivityRegistry);
     }
 
     @Override
@@ -2770,6 +2753,7 @@
         }
 
         super.removeImmediately();
+        mDisplayContent = null;
         mRemoving = false;
     }
 
@@ -3345,13 +3329,6 @@
         mLastSurfaceShowing = show;
     }
 
-    @Override
-    void dump(PrintWriter pw, String prefix, boolean dumpAll) {
-        super.dump(pw, prefix, dumpAll);
-        mAnimatingActivityRegistry.dump(pw, "AnimatingApps:", prefix);
-    }
-
-
     /**
      * Fills in a {@link TaskInfo} with information from this task. Note that the base intent in the
      * task info will not include any extras or clip data.
@@ -6313,10 +6290,6 @@
         return mDisplayContent.getDisplayInfo();
     }
 
-    AnimatingActivityRegistry getAnimatingActivityRegistry() {
-        return mAnimatingActivityRegistry;
-    }
-
     private Rect getRawBounds() {
         return super.getBounds();
     }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index cc14383..ae3a015 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -460,7 +460,7 @@
 
                 // If the previous front-most task is moved to the back, then notify of the new
                 // front-most task.
-                final ActivityRecord topMost = getTopMostActivity();
+                final ActivityRecord topMost = getTopNonFinishingActivity();
                 if (topMost != null) {
                     mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(
                             topMost.getTask().getTaskInfo());
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 64105f6..97a1a34 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -394,6 +394,12 @@
      */
     private boolean mAllowTransitionWhenEmpty;
 
+    /**
+     * Specifies which configuration changes should trigger TaskFragment info changed callbacks.
+     * Only system TaskFragment organizers are allowed to set this value.
+     */
+    private @ActivityInfo.Config int mConfigurationChangeMaskForOrganizer;
+
     /** When set, will force the task to report as invisible. */
     static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1;
     static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1;
@@ -656,6 +662,17 @@
         mAllowTransitionWhenEmpty = allowTransitionWhenEmpty;
     }
 
+    void setConfigurationChangeMaskForOrganizer(@ActivityInfo.Config int mask) {
+        // Only system organizers are allowed to set configuration change mask.
+        if (mTaskFragmentOrganizerController.isSystemOrganizer(mTaskFragmentOrganizer.asBinder())) {
+            mConfigurationChangeMaskForOrganizer = mask;
+        }
+    }
+
+    @ActivityInfo.Config int getConfigurationChangeMaskForOrganizer() {
+        return mConfigurationChangeMaskForOrganizer;
+    }
+
     /** @see #mIsolatedNav */
     boolean isIsolatedNav() {
         return isEmbedded() && mIsolatedNav;
@@ -1224,6 +1241,7 @@
                 false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */);
     }
 
+    @Override
     ActivityRecord getTopNonFinishingActivity() {
         return getTopNonFinishingActivity(
                 true /* includeOverlays */, true /* includeLaunchedFromBubble */);
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index e63107c..ae329d7 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -349,8 +349,10 @@
             // Check if the info is different from the last reported info.
             final TaskFragmentInfo info = tf.getTaskFragmentInfo();
             final TaskFragmentInfo lastInfo = mLastSentTaskFragmentInfos.get(tf);
-            if (info.equalsForTaskFragmentOrganizer(lastInfo) && configurationsAreEqualForOrganizer(
-                    info.getConfiguration(), lastInfo.getConfiguration())) {
+            final int configurationChangeMask = tf.getConfigurationChangeMaskForOrganizer();
+            if (info.equalsForTaskFragmentOrganizer(lastInfo)
+                    && configurationsAreEqualForOrganizer(info.getConfiguration(),
+                            lastInfo.getConfiguration(), configurationChangeMask)) {
                 return null;
             }
 
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 432ed1d..8a93772 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -113,27 +113,6 @@
                 enableLowResSnapshots, lowResScaleFactor, use16BitFormat);
     }
 
-    // Still needed for legacy transition.(AppTransitionControllerTest)
-    void handleClosingApps(ArraySet<ActivityRecord> closingApps) {
-        if (shouldDisableSnapshots()) {
-            return;
-        }
-        // We need to take a snapshot of the task if and only if all activities of the task are
-        // either closing or hidden.
-        mTmpTasks.clear();
-        for (int i = closingApps.size() - 1; i >= 0; i--) {
-            final ActivityRecord activity = closingApps.valueAt(i);
-            if (activity.isActivityTypeHome()) continue;
-            final Task task = activity.getTask();
-            if (task == null) continue;
-
-            getClosingTasksInner(task, mTmpTasks);
-        }
-        snapshotTasks(mTmpTasks);
-        mTmpTasks.clear();
-        mSkipClosingAppSnapshotTasks.clear();
-    }
-
     /**
      * Adds the given {@param tasks} to the list of tasks which should not have their snapshots
      * taken upon the next processing of the set of closing apps. The caller is responsible for
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 563bcb7..25b513d 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -555,6 +555,23 @@
         return null;
     }
 
+    /**
+     * @return The playing transition that is transiently-hiding the given {@param container}, or
+     *         null if there isn't one
+     * @param container A participant of a transient-hide transition
+     */
+    @Nullable
+    Transition getTransientHideTransitionForContainer(
+            @NonNull WindowContainer container) {
+        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+            final Transition transition = mPlayingTransitions.get(i);
+            if (transition.isInTransientHide(container)) {
+                return transition;
+            }
+        }
+        return null;
+    }
+
     /** Returns {@code true} if the display contains a transient-launch transition. */
     boolean hasTransientLaunch(@NonNull DisplayContent dc) {
         if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch()
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 225951d..7af542f 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2079,6 +2079,10 @@
         return getActivity(alwaysTruePredicate(), true /* traverseTopToBottom */);
     }
 
+    ActivityRecord getTopNonFinishingActivity() {
+        return getActivity(r -> !r.finishing, true /* traverseTopToBottom */);
+    }
+
     ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) {
         // Break down into 4 calls to avoid object creation due to capturing input params.
         if (includeFinishing) {
@@ -3355,7 +3359,7 @@
 
     private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter,
                                     boolean isVoiceInteraction) {
-        if (AppTransitionController.isTaskViewTask(this) || (isOrganized()
+        if ((isOrganized()
                 // TODO(b/161711458): Clean-up when moved to shell.
                 && getWindowingMode() != WINDOWING_MODE_FULLSCREEN
                 && getWindowingMode() != WINDOWING_MODE_FREEFORM
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d562666..d699a68 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -157,7 +157,6 @@
 import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER;
 import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID;
 import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
 import static com.android.window.flags.Flags.multiCrop;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
 
@@ -348,7 +347,6 @@
 import com.android.server.DisplayThread;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
-import com.android.server.SystemConfig;
 import com.android.server.UiThread;
 import com.android.server.Watchdog;
 import com.android.server.input.InputManagerService;
@@ -450,11 +448,6 @@
     /**
      * Use WMShell for app transition.
      */
-    private static final String ENABLE_SHELL_TRANSITIONS = "persist.wm.debug.shell_transit";
-
-    /**
-     * @see #ENABLE_SHELL_TRANSITIONS
-     */
     public static final boolean sEnableShellTransitions = getShellTransitEnabled();
 
     /**
@@ -503,6 +496,8 @@
 
     final StartingSurfaceController mStartingSurfaceController;
 
+    final PresentationController mPresentationController;
+
     private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
         @Override
         public void onVrStateChanged(boolean enabled) {
@@ -1433,6 +1428,7 @@
         setGlobalShadowSettings();
         mAnrController = new AnrController(this);
         mStartingSurfaceController = new StartingSurfaceController(this);
+        mPresentationController = new PresentationController();
 
         mBlurController = new BlurController(mContext, mPowerManager);
         mTaskFpsCallbackController = new TaskFpsCallbackController(mContext);
@@ -1937,16 +1933,8 @@
             }
             outSizeCompatScale[0] = win.getCompatScaleForClient();
 
-            if (res >= ADD_OKAY
-                    && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) {
-                displayContent.mIsPresenting = true;
-                if (enablePresentationForConnectedDisplays()) {
-                    // A presentation hides all activities behind on the same display.
-                    displayContent.ensureActivitiesVisible(/*starting=*/ null,
-                            /*notifyClients=*/ true);
-                }
-                mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(),
-                        /*isShown=*/ true);
+            if (res >= ADD_OKAY && win.isPresentation()) {
+                mPresentationController.onPresentationAdded(win);
             }
         }
 
@@ -4732,11 +4720,13 @@
                 }
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES);
-                dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask);
+                final @InsetsType int changedTypes =
+                        dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(
+                                visibleTypes, mask);
                 // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the
                 //  IME provider. Check if we have to create a new request here, if null.
                 dc.getInsetsStateController().onRequestedVisibleTypesChanged(
-                        dc.mRemoteInsetsControlTarget, statsToken);
+                        dc.mRemoteInsetsControlTarget, changedTypes, statsToken);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -10315,11 +10305,6 @@
     }
 
     private static boolean getShellTransitEnabled() {
-        android.content.pm.FeatureInfo autoFeature = SystemConfig.getInstance()
-                .getAvailableFeatures().get(PackageManager.FEATURE_AUTOMOTIVE);
-        if (autoFeature != null && autoFeature.version >= 0) {
-            return SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, true);
-        }
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index a11f4b1..3b6a4dc 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -702,9 +702,23 @@
 
                 if ((entry.getValue().getChangeMask()
                         & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) {
-                    // Disable entering pip (eg. when recents pretends to finish itself)
-                    if (chain.mTransition != null) {
-                        chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */);
+                    if (com.android.wm.shell.Flags.enableRecentsBookendTransition()) {
+                        // If we are using a bookend transition, then the transition that we need
+                        // to disable pip on finish is the original transient transition, not the
+                        // bookend transition
+                        final Transition transientHideTransition =
+                                mTransitionController.getTransientHideTransitionForContainer(wc);
+                        if (transientHideTransition != null) {
+                            transientHideTransition.setCanPipOnFinish(false);
+                        } else {
+                            ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                                    "Set do-not-pip: no task");
+                        }
+                    } else {
+                        // Disable entering pip (eg. when recents pretends to finish itself)
+                        if (chain.mTransition != null) {
+                            chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */);
+                        }
                     }
                 }
                 // A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the
@@ -2441,10 +2455,28 @@
     /** Whether the configuration changes are important to report back to an organizer. */
     static boolean configurationsAreEqualForOrganizer(
             Configuration newConfig, @Nullable Configuration oldConfig) {
+        return configurationsAreEqualForOrganizer(newConfig, oldConfig, 0 /* additionalMask */);
+    }
+
+    /**
+     * Whether the configuration changes are important to report back to an organizer.
+     *
+     * @param newConfig the new configuration
+     * @param oldConfig the old configuration
+     * @param additionalMask specifies additional configuration changes that the organizer is
+     *                       interested in. If the configuration change matches any bit in the mask,
+     *                       {@code false} is returned.
+     */
+    static boolean configurationsAreEqualForOrganizer(
+            Configuration newConfig, @Nullable Configuration oldConfig,
+            @ActivityInfo.Config int additionalMask) {
         if (oldConfig == null) {
             return false;
         }
         int cfgChanges = newConfig.diff(oldConfig);
+        if ((cfgChanges & additionalMask) != 0) {
+            return false;
+        }
         final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0
                 ? (int) newConfig.windowConfiguration.diff(oldConfig.windowConfiguration,
                 true /* compareUndefined */) : 0;
@@ -2651,6 +2683,8 @@
                 ownerActivity.getUid(), ownerActivity.info.processName);
         if (mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder())) {
             taskFragment.setOverrideOrientation(creationParams.getOverrideOrientation());
+            taskFragment.setConfigurationChangeMaskForOrganizer(
+                    creationParams.getConfigurationChangeMask());
         }
         final int position;
         if (creationParams.getPairedPrimaryFragmentToken() != null) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 84d8f84..5897241 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -182,7 +182,6 @@
 import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY;
 import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER;
 import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
 import static com.android.window.flags.Flags.surfaceTrustedOverlay;
 
 import android.annotation.CallSuper;
@@ -822,17 +821,23 @@
     }
 
     /**
+     * @return an integer as the changed requested visible insets types.
      * @see #getRequestedVisibleTypes()
      */
-    void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
+    @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
         if (mRequestedVisibleTypes != requestedVisibleTypes) {
+            final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes;
             mRequestedVisibleTypes = requestedVisibleTypes;
+            return changedTypes;
         }
+        return 0;
     }
 
     @VisibleForTesting
-    void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) {
-        setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
+    @InsetsType int setRequestedVisibleTypes(
+            @InsetsType int requestedVisibleTypes, @InsetsType int mask) {
+        return setRequestedVisibleTypes(
+                mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
     }
 
     /**
@@ -2069,38 +2074,15 @@
         super.onMovedByResize();
     }
 
-    void onAppVisibilityChanged(boolean visible, boolean runningAppAnimation) {
+    void onAppCommitInvisible() {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
-            mChildren.get(i).onAppVisibilityChanged(visible, runningAppAnimation);
+            mChildren.get(i).onAppCommitInvisible();
         }
-
-        final boolean isVisibleNow = isVisibleNow();
-        if (mAttrs.type == TYPE_APPLICATION_STARTING) {
-            // Starting window that's exiting will be removed when the animation finishes.
-            // Mark all relevant flags for that onExitAnimationDone will proceed all the way
-            // to actually remove it.
-            if (!visible && isVisibleNow && mActivityRecord.isAnimating(PARENTS | TRANSITION)) {
-                ProtoLog.d(WM_DEBUG_ANIM,
-                        "Set animatingExit: reason=onAppVisibilityChanged win=%s", this);
-                mAnimatingExit = true;
-                mRemoveOnExit = true;
-                mWindowRemovalAllowed = true;
-            }
-        } else if (visible != isVisibleNow) {
-            // Run exit animation if:
-            // 1. App visibility and WS visibility are different
-            // 2. App is not running an animation
-            // 3. WS is currently visible
-            if (!runningAppAnimation && isVisibleNow) {
-                final AccessibilityController accessibilityController =
-                        mWmService.mAccessibilityController;
-                final int winTransit = TRANSIT_EXIT;
-                mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */);
-                if (accessibilityController.hasCallbacks()) {
-                    accessibilityController.onWindowTransition(this, winTransit);
-                }
-            }
-            setDisplayLayoutNeeded();
+        if (mAttrs.type != TYPE_APPLICATION_STARTING
+                && mWmService.mAccessibilityController.hasCallbacks()
+                // It is a change only if App visibility and WS visibility are different.
+                && isVisible()) {
+            mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT);
         }
     }
 
@@ -2317,15 +2299,8 @@
 
         final int type = mAttrs.type;
 
-        if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) {
-            // TODO(b/393945496): Make sure that there's one presentation at most per display.
-            dc.mIsPresenting = false;
-            if (enablePresentationForConnectedDisplays()) {
-                // A presentation hides all activities behind on the same display.
-                dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true);
-            }
-            mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(),
-                    /*isShown=*/ false);
+        if (isPresentation()) {
+            mWmService.mPresentationController.onPresentationRemoved(this);
         }
         // Check if window provides non decor insets before clearing its provided insets.
         final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets();
@@ -3354,6 +3329,10 @@
         }
     }
 
+    boolean isPresentation() {
+        return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION;
+    }
+
     private boolean isOnVirtualDisplay() {
         return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL;
     }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 883cab0..f07e672 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -668,8 +668,10 @@
     }
 
     // TODO(b/383092013): Add topology validation
-    mInputManager->getChoreographer().setDisplayTopology(
-            android_hardware_display_DisplayTopologyGraph_toNative(env, topologyGraph));
+    const DisplayTopologyGraph displayTopology =
+            android_hardware_display_DisplayTopologyGraph_toNative(env, topologyGraph);
+    mInputManager->getDispatcher().setDisplayTopology(displayTopology);
+    mInputManager->getChoreographer().setDisplayTopology(displayTopology);
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputChannel(
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index 2aa0c6b..440eae5 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -446,7 +446,7 @@
         @Override
         public void binderDied() {
             Slog.d(TAG, "Client binder died - clearing session");
-            finishSession(isUiWaitingForData(), ApiStatus.CLIENT_CANCELED.getMetricCode());
+            finishSession(isUiWaitingForData(), ApiStatus.BINDER_DIED.getMetricCode());
         }
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
index ece729f..c21e645 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
@@ -16,6 +16,7 @@
 
 package com.android.server.credentials.metrics;
 
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_BINDER_DIED;
 import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_CLIENT_CANCELED;
 import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_FAILURE;
 import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_SUCCESS;
@@ -27,7 +28,9 @@
     CLIENT_CANCELED(
             CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_CLIENT_CANCELED),
     USER_CANCELED(
-            CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_USER_CANCELED);
+            CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_USER_CANCELED),
+    BINDER_DIED(
+            CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_BINDER_DIED);
 
     private final int mInnerMetricCode;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e11c31c..34f8ac6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -15993,8 +15993,6 @@
         @Override
         public void notifyUnsafeOperationStateChanged(DevicePolicySafetyChecker checker, int reason,
                 boolean isSafe) {
-            // TODO(b/178494483): use EventLog instead
-            // TODO(b/178494483): log metrics?
             if (VERBOSE_LOG) {
                 Slogf.v(LOG_TAG, "notifyUnsafeOperationStateChanged(): %s=%b",
                         DevicePolicyManager.operationSafetyReasonToString(reason), isSafe);
@@ -16006,16 +16004,20 @@
             extras.putInt(DeviceAdminReceiver.EXTRA_OPERATION_SAFETY_REASON, reason);
             extras.putBoolean(DeviceAdminReceiver.EXTRA_OPERATION_SAFETY_STATE, isSafe);
 
-            if (mOwners.hasDeviceOwner()) {
-                if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Notifying DO");
-                sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_OPERATION_SAFETY_STATE_CHANGED,
-                        extras);
-            }
-            for (int profileOwnerId : mOwners.getProfileOwnerKeys()) {
-                if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Notifying PO for user " + profileOwnerId);
-                sendProfileOwnerCommand(DeviceAdminReceiver.ACTION_OPERATION_SAFETY_STATE_CHANGED,
-                        extras, profileOwnerId);
-            }
+            mInjector.binderWithCleanCallingIdentity(() -> {
+                if (mOwners.hasDeviceOwner()) {
+                    if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Notifying DO");
+                    sendDeviceOwnerCommand(
+                            DeviceAdminReceiver.ACTION_OPERATION_SAFETY_STATE_CHANGED,
+                            extras);
+                }
+                for (int profileOwnerId : mOwners.getProfileOwnerKeys()) {
+                    if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Notifying PO for user " + profileOwnerId);
+                    sendProfileOwnerCommand(
+                            DeviceAdminReceiver.ACTION_OPERATION_SAFETY_STATE_CHANGED,
+                            extras, profileOwnerId);
+                }
+            });
         }
 
         private @Mode int findInteractAcrossProfilesResetMode(String packageName) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
index 6e038f9..ba02122 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
@@ -54,17 +54,7 @@
         TelephonyManager telephonyService = context.getSystemService(TelephonyManager.class);
         Preconditions.checkState(telephonyService != null, "Unable to access telephony service");
 
-        String imei;
-        try {
-            imei = telephonyService.getImei(0);
-        } catch (UnsupportedOperationException doesNotSupportGms) {
-            // Instead of catching the exception, we could check for FEATURE_TELEPHONY_GSM.
-            // However that runs the risk of changing a device's existing ESID if on these devices
-            // telephonyService.getImei() actually returns non-null even when the device does not
-            // declare FEATURE_TELEPHONY_GSM.
-            imei = null;
-        }
-        mImei = imei;
+        mImei = telephonyService.getImei(0);
         String meid;
         try {
             meid = telephonyService.getMeid(0);
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index dae481a..36947a2 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -3198,8 +3198,10 @@
     dprintf(fd, "    }\n");
 }
 
-void IncrementalService::AppOpsListener::opChanged(int32_t, const String16&) {
+binder::Status IncrementalService::AppOpsListener::opChanged(int32_t, int32_t,
+                                                             const String16&, const String16&) {
     incrementalService.onAppOpChanged(packageName);
+    return binder::Status::ok();
 }
 
 binder::Status IncrementalService::IncrementalServiceConnector::setStorageParams(
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index b81e1b1..4ee1a70 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -26,7 +26,7 @@
 #include <android/os/incremental/BnStorageLoadingProgressListener.h>
 #include <android/os/incremental/PerUidReadTimeouts.h>
 #include <android/os/incremental/StorageHealthCheckParams.h>
-#include <binder/IAppOpsCallback.h>
+#include <binder/AppOpsManager.h>
 #include <binder/PersistableBundle.h>
 #include <utils/String16.h>
 #include <utils/StrongPointer.h>
@@ -200,11 +200,12 @@
 
     void getMetrics(int32_t storageId, android::os::PersistableBundle* _aidl_return);
 
-    class AppOpsListener : public android::BnAppOpsCallback {
+    class AppOpsListener : public com::android::internal::app::BnAppOpsCallback {
     public:
         AppOpsListener(IncrementalService& incrementalService, std::string packageName)
               : incrementalService(incrementalService), packageName(std::move(packageName)) {}
-        void opChanged(int32_t op, const String16& packageName) final;
+        binder::Status opChanged(int32_t op, int32_t uid, const String16& packageName,
+                                 const String16& persistentDeviceId) final;
 
     private:
         IncrementalService& incrementalService;
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index 39e2ee3..36a5b7f 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -23,7 +23,7 @@
 #include <android/content/pm/IDataLoader.h>
 #include <android/content/pm/IDataLoaderStatusListener.h>
 #include <android/os/incremental/PerUidReadTimeouts.h>
-#include <binder/IAppOpsCallback.h>
+#include <binder/AppOpsManager.h>
 #include <binder/IServiceManager.h>
 #include <binder/Status.h>
 #include <incfs.h>
@@ -133,6 +133,7 @@
 
 class AppOpsManagerWrapper {
 public:
+    using IAppOpsCallback = ::com::android::internal::app::IAppOpsCallback;
     virtual ~AppOpsManagerWrapper() = default;
     virtual binder::Status checkPermission(const char* permission, const char* operation,
                                            const char* package) const = 0;
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index d9d3d62..73849a3 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -1678,7 +1678,7 @@
                                                   {}, {}));
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get());
-    mAppOpsManager->mStoredCallback->opChanged(0, {});
+    mAppOpsManager->mStoredCallback->opChanged(0, 0, {}, {});
 }
 
 TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsCheckPermissionFails) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c974d9e..2bbd69c 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -894,6 +894,17 @@
 
             SystemServiceRegistry.sEnableServiceNotFoundWtf = true;
 
+            // Prepare the thread pool for init tasks that can be parallelized
+            SystemServerInitThreadPool tp = SystemServerInitThreadPool.start();
+            mDumper.addDumpable(tp);
+
+            if (android.server.Flags.earlySystemConfigInit()) {
+                // SystemConfig init is expensive, so enqueue the work as early as possible to allow
+                // concurrent execution before it's needed (typically by ActivityManagerService).
+                // As native library loading is also expensive, this is a good place to start.
+                startSystemConfigInit(t);
+            }
+
             // Initialize native services.
             System.loadLibrary("android_servers");
 
@@ -926,9 +937,6 @@
             mDumper.addDumpable(mSystemServiceManager);
 
             LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
-            // Prepare the thread pool for init tasks that can be parallelized
-            SystemServerInitThreadPool tp = SystemServerInitThreadPool.start();
-            mDumper.addDumpable(tp);
 
             // Lazily load the pre-installed system font map in SystemServer only if we're not doing
             // the optimized font loading in the FontManagerService.
@@ -1093,6 +1101,14 @@
         }
     }
 
+    private void startSystemConfigInit(TimingsTraceAndSlog t) {
+        Slog.i(TAG, "Reading configuration...");
+        final String tagSystemConfig = "ReadingSystemConfig";
+        t.traceBegin(tagSystemConfig);
+        SystemServerInitThreadPool.submit(SystemConfig::getInstance, tagSystemConfig);
+        t.traceEnd();
+    }
+
     private void createSystemContext() {
         ActivityThread activityThread = ActivityThread.systemMain();
         mSystemContext = activityThread.getSystemContext();
@@ -1131,11 +1147,11 @@
         mDumper.addDumpable(watchdog);
         t.traceEnd();
 
-        Slog.i(TAG, "Reading configuration...");
-        final String TAG_SYSTEM_CONFIG = "ReadingSystemConfig";
-        t.traceBegin(TAG_SYSTEM_CONFIG);
-        SystemServerInitThreadPool.submit(SystemConfig::getInstance, TAG_SYSTEM_CONFIG);
-        t.traceEnd();
+        // Legacy entry point for starting SystemConfig init, only needed if the early init flag is
+        // disabled and we haven't already triggered init before bootstrap services.
+        if (!android.server.Flags.earlySystemConfigInit()) {
+            startSystemConfigInit(t);
+        }
 
         // Orchestrates some ProtoLogging functionality.
         if (android.tracing.Flags.clientSideProtoLogging()) {
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 4d021ec..86ccd87 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -10,6 +10,13 @@
 }
 
 flag {
+     namespace: "system_performance"
+     name: "early_system_config_init"
+     description: "Perform earlier initialization of SystemConfig in system server startup."
+     bug: "383869534"
+}
+
+flag {
      name: "remove_text_service"
      namespace: "wear_frameworks"
      description: "Remove TextServiceManagerService on Wear"
diff --git a/services/proguard.flags b/services/proguard.flags
index 0e1f68e..8d8b418 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -15,7 +15,10 @@
 
 # APIs referenced by dependent JAR files and modules
 # TODO(b/300514883): Pull @SystemApi keep rules from system-api.pro.
--keep interface android.annotation.SystemApi
+# TODO(b/373579455): Evaluate if <init> needs to be kept.
+-keep interface android.annotation.SystemApi {
+  void <init>();
+}
 -keep @android.annotation.SystemApi class * {
   public protected *;
 }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
index 66aaa562..a01df8b 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
@@ -37,6 +37,7 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.content.pm.UserInfo;
+import android.app.PropertyInvalidatedCache;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Message;
@@ -50,6 +51,8 @@
 
 import androidx.annotation.NonNull;
 
+import android.app.ApplicationPackageManager;
+import android.content.pm.PackageManager;
 import com.android.internal.pm.parsing.pkg.PackageImpl;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.pkg.component.ParsedActivity;
@@ -64,8 +67,10 @@
 import com.android.internal.pm.pkg.parsing.ParsingPackage;
 import com.android.server.om.OverlayReferenceMapper;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.utils.Watchable;
 import com.android.server.utils.WatchableTester;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -244,6 +249,55 @@
                 (Answer<Boolean>) invocation ->
                         ((AndroidPackage) invocation.getArgument(SYSTEM_USER)).getTargetSdkVersion()
                                 >= Build.VERSION_CODES.R);
+        PropertyInvalidatedCache.setTestMode(true);
+        PackageManager.sApplicationInfoCache.testPropertyName();
+        ApplicationPackageManager.sGetPackagesForUidCache.testPropertyName();
+    }
+
+    @After
+    public void tearDown() {
+        PropertyInvalidatedCache.setTestMode(false);
+    }
+
+    /**
+     * A class to make it easier to verify that PM caches are properly invalidated by
+     * AppsFilterImpl operations.  This extends WatchableTester to test the cache nonces along
+     * with change reporting.
+     */
+    private static class NonceTester extends WatchableTester {
+        // The nonces from caches under consideration.  The no-parameter constructor fetches the
+        // values from the cacches.
+        private static record Nonces(long applicationInfo, long packageInfo) {
+            Nonces() {
+                this(ApplicationPackageManager.sGetPackagesForUidCache.getNonce(),
+                        PackageManager.sApplicationInfoCache.getNonce());
+            }
+        }
+
+        // Track the latest cache nonces.
+        private Nonces mNonces;
+
+        NonceTester(Watchable w, String k) {
+            super(w, k);
+            mNonces = new Nonces();
+        }
+
+        @Override
+        public void verifyChangeReported(String msg) {
+            super.verifyChangeReported(msg);
+            Nonces update = new Nonces();
+            assertTrue(msg, update.applicationInfo != mNonces.applicationInfo);
+            assertTrue(msg, update.packageInfo != mNonces.packageInfo);
+            mNonces = update;
+        }
+
+        @Override
+        public void verifyNoChangeReported(String msg) {
+            super.verifyNoChangeReported(msg);
+            Nonces update = new Nonces();
+            assertTrue(msg, update.applicationInfo == mNonces.applicationInfo);
+            assertTrue(msg, update.packageInfo == mNonces.packageInfo);
+        }
     }
 
     @Test
@@ -1167,7 +1221,7 @@
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, /* systemAppsQueryable */
                         false, /* overlayProvider */ null, mMockHandler);
-        final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
+        final WatchableTester watcher = new NonceTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
         watcher.verifyChangeReported("addBasicAndroid");
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 5393e20..b9cea0c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -388,6 +388,34 @@
                 PORT_C, false);
     }
 
+    /**
+     * Confirm that display is marked as trusted, has own focus, disables steal top focus when it
+     * is listed in com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts.
+     */
+    @Test
+    public void testStealTopFocusDisabledDisplay() throws Exception {
+        setUpDisplay(new FakeDisplay(PORT_A));
+        setUpDisplay(new FakeDisplay(PORT_B));
+        setUpDisplay(new FakeDisplay(PORT_C));
+        updateAvailableDisplays();
+
+        doReturn(new int[]{ PORT_B }).when(mMockedResources).getIntArray(
+                com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts);
+        mAdapter.registerLocked();
+
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        // This should not have the flags
+        assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(),
+                PORT_A, false);
+        // This should have the flags
+        assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(),
+                PORT_B, true);
+        // This should not have the flags
+        assertNotStealTopFocusFlag(mListener.addedDisplays.get(2).getDisplayDeviceInfoLocked(),
+                PORT_C, false);
+    }
+
     @Test
     public void testSupportedDisplayModesGetOverriddenWhenDisplayIsUpdated()
             throws InterruptedException {
@@ -452,6 +480,42 @@
     }
 
     /**
+     * Confirm that all local displays are not trusted, do not have their own focus, and do not
+     * steal top focus when config_localNotStealTopFocusDisplayPorts is empty:
+     */
+    @Test
+    public void testDisplayFlagsForNoConfigLocalNotStealTopFocusDisplayPorts() throws Exception {
+        setUpDisplay(new FakeDisplay(PORT_A));
+        setUpDisplay(new FakeDisplay(PORT_C));
+        updateAvailableDisplays();
+
+        // config_localNotStealTopFocusDisplayPorts is null
+        mAdapter.registerLocked();
+
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        // This should not have the flags
+        assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(),
+                PORT_A, false);
+        // This should not have the flags
+        assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(),
+                PORT_C, false);
+    }
+
+    private static void assertNotStealTopFocusFlag(
+            DisplayDeviceInfo info, int expectedPort, boolean shouldHaveFlags) {
+        final DisplayAddress.Physical address = (DisplayAddress.Physical) info.address;
+        assertNotNull(address);
+        assertEquals(expectedPort, address.getPort());
+        assertEquals(DISPLAY_MODEL, address.getModel());
+        assertEquals(shouldHaveFlags,
+                (info.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0);
+        assertEquals(shouldHaveFlags, (info.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0);
+        // display is always trusted since it is created by the system
+        assertEquals(true, (info.flags & DisplayDeviceInfo.FLAG_TRUSTED) != 0);
+    }
+
+    /**
      * Confirm that external display uses physical density.
      */
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index f154dbc..09ce263 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -3962,7 +3962,7 @@
         }
 
         @Override
-        public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) {
+        public VotesStatsReporter getVotesStatsReporter() {
             return null;
         }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
index 2e4b97e..371b0c9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
 
+import android.app.PropertyInvalidatedCache;
 import android.content.Context;
 import android.multiuser.Flags;
 import android.os.UserManager;
@@ -75,6 +76,8 @@
 
     @Before
     public void setFixtures() {
+        PropertyInvalidatedCache.disableForTestMode();
+
         // Called when WatchedUserStates is constructed
         doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache());
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 1e665c2..409706b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1550,6 +1550,118 @@
         verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2));
     }
 
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryGroupPolicy_sameAction_multiplePolicies() {
+        // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_GREEN
+        // package.
+        final Intent greenPackageChangedIntent = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN));
+        // Create delivery group policy such that when there are multiple broadcasts within the
+        // delivery group identified by "com.example.green/10002", only the most recent one
+        // gets delivered and the rest get discarded.
+        final BroadcastOptions optionsMostRecentPolicyForPackageGreen =
+                BroadcastOptions.makeBasic();
+        optionsMostRecentPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+                PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN));
+        optionsMostRecentPolicyForPackageGreen.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+        // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_RED
+        // package.
+        final Intent redPackageChangedIntent = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_RED), List.of(PACKAGE_RED));
+        // Create delivery group policy such that when there are multiple broadcasts within the
+        // delivery group identified by "com.example.red/10001", only the most recent one
+        // gets delivered and the rest get discarded.
+        final BroadcastOptions optionsMostRecentPolicyForPackageRed =
+                BroadcastOptions.makeBasic();
+        optionsMostRecentPolicyForPackageRed.setDeliveryGroupMatchingKey("package_changed",
+                PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED));
+        optionsMostRecentPolicyForPackageRed.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+        // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of
+        // PACKAGE_GREEN package.
+        final Intent greenPackageComponentsChangedIntent1 = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_GREEN),
+                List.of(PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2"));
+        final Intent greenPackageComponentsChangedIntent2 = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_GREEN),
+                List.of(PACKAGE_GREEN + ".comp3"));
+        // Create delivery group policy such that when there are multiple broadcasts within the
+        // delivery group identified by "components-com.example.green/10002", merge the extras
+        // within these broadcasts such that only one broadcast is sent and the rest are
+        // discarded. Couple of things to note here:
+        // 1. We are intentionally using a different policy group
+        //    "components-com.example.green/10002" (as opposed to "com.example.green/10002" used
+        //    earlier), because this is corresponding to a change in some particular components,
+        //    rather than a change to the whole package and we want to keep these two types of
+        //    broadcasts independent.
+        // 2. We are using 'extrasMerger' to indicate how we want the extras to be merged. This
+        //    assumes that broadcasts belonging to the group 'components-com.example.green/10002'
+        //    will have the same values for all the extras, except for the one extra
+        //    'EXTRA_CHANGED_COMPONENT_NAME_LIST'. So, we explicitly specify how to merge this
+        //    extra by using 'STRATEGY_ARRAY_APPEND' strategy, which basically indicates that
+        //    the extra values which are arrays should be concatenated.
+        final BundleMerger extrasMerger = new BundleMerger();
+        extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+                BundleMerger.STRATEGY_ARRAY_APPEND);
+        final BroadcastOptions optionsMergedPolicyForPackageGreen = BroadcastOptions.makeBasic();
+        optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+                "components-" + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN));
+        optionsMergedPolicyForPackageGreen.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+        optionsMergedPolicyForPackageGreen.setDeliveryGroupExtrasMerger(extrasMerger);
+
+        // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of
+        // PACKAGE_RED package.
+        final Intent redPackageComponentsChangedIntent = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_RED),
+                List.of(PACKAGE_RED + ".comp1", PACKAGE_RED + ".comp2"));
+        // Create delivery group policy such that when there are multiple broadcasts within the
+        // delivery group identified by "components-com.example.red/10001", merge the extras
+        // within these broadcasts such that only one broadcast is sent and the rest are
+        // discarded.
+        final BroadcastOptions optionsMergedPolicyForPackageRed = BroadcastOptions.makeBasic();
+        optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+                "components-" + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED));
+        optionsMergedPolicyForPackageRed.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+        optionsMergedPolicyForPackageRed.setDeliveryGroupExtrasMerger(extrasMerger);
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent,
+                optionsMostRecentPolicyForPackageGreen));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageChangedIntent,
+                optionsMostRecentPolicyForPackageRed));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent1,
+                optionsMergedPolicyForPackageGreen));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageComponentsChangedIntent,
+                optionsMergedPolicyForPackageRed));
+        // Since this broadcast has DELIVERY_GROUP_MOST_RECENT policy set, the earlier
+        // greenPackageChangedIntent broadcast with the same policy will be discarded.
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent,
+                optionsMostRecentPolicyForPackageGreen));
+        // Since this broadcast has DELIVERY_GROUP_MERGED policy set, the earlier
+        // greenPackageComponentsChangedIntent1 broadcast with the same policy will be merged
+        // with this one and then will be discarded.
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent2,
+                optionsMergedPolicyForPackageGreen));
+
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        // The extra EXTRA_CHANGED_COMPONENT_NAME_LIST values from
+        // greenPackageComponentsChangedIntent1 and
+        // greenPackageComponentsChangedIntent2 broadcasts would be merged, since
+        // STRATEGY_ARRAY_APPEND was used for this extra.
+        final Intent expectedGreenPackageComponentsChangedIntent = createPackageChangedIntent(
+                getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN + ".comp3",
+                        PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2"));
+        verifyPendingRecords(queue, List.of(redPackageChangedIntent,
+                redPackageComponentsChangedIntent, greenPackageChangedIntent,
+                expectedGreenPackageComponentsChangedIntent));
+    }
+
     private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs,
             int droppedCount) {
         final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 86bf203..409b114 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -27,6 +27,7 @@
 import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
@@ -73,6 +74,7 @@
     }
 
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
     public void onCreate_systemUser_addsAllHelpers() {
         UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
         when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -94,10 +96,12 @@
                         "app_gender",
                         "companion",
                         "system_gender",
-                        "display");
+                        "display",
+                        "input");
     }
 
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
     public void onCreate_systemUser_slicesDisabled_addsAllNonSlicesHelpers() {
         UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
         when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -120,10 +124,12 @@
                         "app_gender",
                         "companion",
                         "system_gender",
-                        "display");
+                        "display",
+                        "input");
     }
 
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
     public void onCreate_profileUser_addsProfileEligibleHelpers() {
         UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
         when(mUserManagerMock.isProfile()).thenReturn(true);
@@ -143,6 +149,7 @@
     }
 
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
     public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() {
         UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
         when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -162,7 +169,8 @@
                         "companion",
                         "app_gender",
                         "system_gender",
-                        "display");
+                        "display",
+                        "input");
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index f79cb11..360d6eb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -19,6 +19,11 @@
 import static android.content.pm.PackageManager.FEATURE_EMBEDDED;
 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.multiuser.Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION;
+import static android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES;
+import static android.multiuser.Flags.FLAG_LOGOUT_USER_API;
+import static android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE;
+import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
 import static android.os.UserManager.DISALLOW_OUTGOING_CALLS;
 import static android.os.UserManager.DISALLOW_SMS;
 import static android.os.UserManager.DISALLOW_USER_SWITCH;
@@ -54,7 +59,6 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
-import android.multiuser.Flags;
 import android.os.PowerManager;
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
@@ -401,15 +405,27 @@
     }
 
     @Test
-    public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception {
+    public void testGetBootUser_CannotSwitchToHeadlessSystemUser_ThrowsIfOnlySystemUserExists()
+            throws Exception {
         setSystemUserHeadless(true);
         removeNonSystemUsers();
+        mockCanSwitchToHeadlessSystemUser(false);
 
         assertThrows(UserManager.CheckedUserOperationException.class,
                 () -> mUmi.getBootUser(/* waitUntilSet= */ false));
     }
 
     @Test
+    public void testGetBootUser_CanSwitchToHeadlessSystemUser_NoThrowIfOnlySystemUserExists()
+            throws Exception {
+        setSystemUserHeadless(true);
+        removeNonSystemUsers();
+        mockCanSwitchToHeadlessSystemUser(true);
+
+        assertThat(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(UserHandle.USER_SYSTEM);
+    }
+
+    @Test
     public void testGetPreviousFullUserToEnterForeground() throws Exception {
         addUser(USER_ID);
         setLastForegroundTime(USER_ID, 1_000_000L);
@@ -601,9 +617,8 @@
     }
 
     @Test
+    @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testAutoLockPrivateProfile() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         int mainUser = mUms.getMainUserId();
         assumeTrue(mUms.canAddPrivateProfile(mainUser));
         UserManagerService mSpiedUms = spy(mUms);
@@ -622,10 +637,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE
+    })
     public void testAutoLockOnDeviceLockForPrivateProfile() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         int mainUser = mUms.getMainUserId();
         assumeTrue(mUms.canAddPrivateProfile(mainUser));
         UserManagerService mSpiedUms = spy(mUms);
@@ -645,10 +662,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE
+    })
     public void testAutoLockOnDeviceLockForPrivateProfile_keyguardUnlocked() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         assumeTrue(mUms.canAddPrivateProfile(0));
         UserManagerService mSpiedUms = spy(mUms);
         UserInfo privateProfileUser =
@@ -665,10 +684,9 @@
     }
 
     @Test
+    @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @DisableFlags(FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE)
     public void testAutoLockOnDeviceLockForPrivateProfile_flagDisabled() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.disableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         int mainUser = mUms.getMainUserId();
         assumeTrue(mUms.canAddPrivateProfile(mainUser));
         UserManagerService mSpiedUms = spy(mUms);
@@ -687,10 +705,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE
+    })
     public void testAutoLockAfterInactityForPrivateProfile() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         int mainUser = mUms.getMainUserId();
         assumeTrue(mUms.canAddPrivateProfile(mainUser));
         UserManagerService mSpiedUms = spy(mUms);
@@ -711,11 +731,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE
+    })
     public void testSetOrUpdateAutoLockPreference_noPrivateProfile() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
-
         mUms.setOrUpdateAutoLockPreferenceForPrivateProfile(
                 Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY);
 
@@ -726,10 +747,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE
+    })
     public void testSetOrUpdateAutoLockPreference() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         int mainUser = mUms.getMainUserId();
         assumeTrue(mUms.canAddPrivateProfile(mainUser));
         mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
@@ -780,10 +803,12 @@
     }
 
     @Test
+    @EnableFlags({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+        android.multiuser.Flags.FLAG_ENABLE_HIDING_PROFILES
+    })
     public void testGetProfileIdsExcludingHidden() {
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_HIDING_PROFILES);
         assumeTrue(mUms.canAddPrivateProfile(0));
         UserInfo privateProfileUser =
                 mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
@@ -794,8 +819,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() {
         UserManagerService mSpiedUms = spy(mUms);
         assumeTrue(mUms.isHeadlessSystemUserMode());
@@ -807,8 +835,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnSecondaryUser_shouldNotAllowCreation() {
         assumeTrue(mUms.canAddMoreUsersOfType(USER_TYPE_FULL_SECONDARY));
         UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0);
@@ -819,8 +850,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnAutoDevices_shouldNotAllowCreation() {
         doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_AUTOMOTIVE), anyInt());
         int mainUser = mUms.getMainUserId();
@@ -831,8 +865,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnTV_shouldNotAllowCreation() {
         doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_LEANBACK), anyInt());
         int mainUser = mUms.getMainUserId();
@@ -843,8 +880,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnEmbedded_shouldNotAllowCreation() {
         doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_EMBEDDED), anyInt());
         int mainUser = mUms.getMainUserId();
@@ -855,8 +895,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-            Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
+    @RequiresFlagsEnabled({
+        FLAG_ALLOW_PRIVATE_PROFILE,
+        FLAG_BLOCK_PRIVATE_SPACE_CREATION,
+        FLAG_ENABLE_PRIVATE_SPACE_FEATURES
+    })
     public void testCreatePrivateProfileOnWatch_shouldNotAllowCreation() {
         doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_WATCH), anyInt());
         int mainUser = mUms.getMainUserId();
@@ -910,7 +953,7 @@
     }
 
     @Test
-    @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @EnableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_HsumAndInteractiveHeadlessSystem_UserCanLogout()
             throws Exception {
         setSystemUserHeadless(true);
@@ -926,7 +969,7 @@
     }
 
     @Test
-    @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @EnableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_HsumAndNonInteractiveHeadlessSystem_UserCannotLogout()
             throws Exception {
         setSystemUserHeadless(true);
@@ -941,7 +984,7 @@
     }
 
     @Test
-    @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @EnableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_Hsum_SystemUserCannotLogout() throws Exception {
         setSystemUserHeadless(true);
         mockCurrentUser(UserHandle.USER_SYSTEM);
@@ -950,7 +993,7 @@
     }
 
     @Test
-    @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @EnableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_NonHsum_SystemUserCannotLogout() throws Exception {
         setSystemUserHeadless(false);
         mockCurrentUser(UserHandle.USER_SYSTEM);
@@ -960,7 +1003,7 @@
     }
 
     @Test
-    @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @EnableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_CannotSwitch_CannotLogout() throws Exception {
         setSystemUserHeadless(true);
         addUser(USER_ID);
@@ -973,7 +1016,7 @@
     }
 
     @Test
-    @DisableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+    @DisableFlags(FLAG_LOGOUT_USER_API)
     public void testGetUserLogoutability_LogoutDisabled() throws Exception {
         assertThrows(UnsupportedOperationException.class, () -> mUms.getUserLogoutability(USER_ID));
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index cd36539..bc04fd9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -148,6 +148,7 @@
 
     private static ComponentName sImageWallpaperComponentName;
     private static ComponentName sDefaultWallpaperComponent;
+    private static WallpaperDescription sDefaultWallpaperDescription;
 
     private static ComponentName sFallbackWallpaperComponentName;
 
@@ -214,6 +215,8 @@
         } else {
             sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService);
         }
+        sDefaultWallpaperDescription = new WallpaperDescription.Builder().setComponent(
+                sDefaultWallpaperComponent).build();
 
         sContext.addMockService(sImageWallpaperComponentName, sWallpaperService);
         sContext.addMockService(TEST_WALLPAPER_COMPONENT, sWallpaperService);
@@ -489,11 +492,12 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+    @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+            Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
     public void testSaveLoadSettings_withoutWallpaperDescription()
             throws IOException, XmlPullParserException {
         WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
-        expectedData.setComponent(sDefaultWallpaperComponent);
+        expectedData.setDescription(sDefaultWallpaperDescription);
         expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
         expectedData.mWallpaperDimAmount = 0.5f;
@@ -529,11 +533,12 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+    @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+            Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
     public void testSaveLoadSettings_withWallpaperDescription()
             throws IOException, XmlPullParserException {
         WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
-        expectedData.setComponent(sDefaultWallpaperComponent);
+        expectedData.setDescription(sDefaultWallpaperDescription);
         PersistableBundle content = new PersistableBundle();
         content.putString("ckey", "cvalue");
         WallpaperDescription description = new WallpaperDescription.Builder()
@@ -561,7 +566,8 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+    @DisableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+            Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
     public void testSaveLoadSettings_legacyNextComponent()
             throws IOException, XmlPullParserException {
         WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
index c824c39..c7c23f0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
@@ -1,3 +1,6 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
 
 include /core/java/android/view/accessibility/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 457fde8d..0227ef1 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -85,7 +85,7 @@
     public void onMotionEvent_lazyInitClickScheduler() {
         assertThat(mController.mClickScheduler).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mClickScheduler).isNotNull();
     }
@@ -94,7 +94,7 @@
     public void onMotionEvent_nonMouseSource_notInitClickScheduler() {
         assertThat(mController.mClickScheduler).isNull();
 
-        injectFakeNonMouseActionDownEvent();
+        injectFakeNonMouseActionHoverMoveEvent();
 
         assertThat(mController.mClickScheduler).isNull();
     }
@@ -103,7 +103,7 @@
     public void onMotionEvent_lazyInitAutoclickSettingsObserver() {
         assertThat(mController.mAutoclickSettingsObserver).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickSettingsObserver).isNotNull();
     }
@@ -113,7 +113,7 @@
     public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() {
         assertThat(mController.mAutoclickIndicatorScheduler).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickIndicatorScheduler).isNotNull();
     }
@@ -123,7 +123,7 @@
     public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() {
         assertThat(mController.mAutoclickIndicatorScheduler).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickIndicatorScheduler).isNull();
     }
@@ -133,7 +133,7 @@
     public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() {
         assertThat(mController.mAutoclickIndicatorView).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickIndicatorView).isNotNull();
     }
@@ -143,7 +143,7 @@
     public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() {
         assertThat(mController.mAutoclickIndicatorView).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickIndicatorView).isNull();
     }
@@ -153,7 +153,7 @@
     public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() {
         assertThat(mController.mAutoclickTypePanel).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickTypePanel).isNotNull();
     }
@@ -163,7 +163,7 @@
     public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() {
         assertThat(mController.mAutoclickTypePanel).isNull();
 
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         assertThat(mController.mAutoclickTypePanel).isNull();
     }
@@ -171,7 +171,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onMotionEvent_flagOn_addAutoclickIndicatorViewToWindowManager() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         verify(mMockWindowManager).addView(eq(mController.mAutoclickIndicatorView), any());
     }
@@ -179,7 +179,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onDestroy_flagOn_removeAutoclickIndicatorViewToWindowManager() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         mController.onDestroy();
 
@@ -189,7 +189,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
         AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
         mController.mAutoclickTypePanel = mockAutoclickTypePanel;
 
@@ -200,7 +200,7 @@
 
     @Test
     public void onMotionEvent_initClickSchedulerDelayFromSetting() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         int delay =
                 Settings.Secure.getIntForUser(
@@ -214,7 +214,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onMotionEvent_flagOn_initCursorAreaSizeFromSetting() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         int size =
                 Settings.Secure.getIntForUser(
@@ -238,7 +238,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
         injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState);
@@ -250,7 +250,7 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON);
 
@@ -260,7 +260,7 @@
 
     @Test
     public void onDestroy_clearClickScheduler() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         mController.onDestroy();
 
@@ -269,7 +269,7 @@
 
     @Test
     public void onDestroy_clearAutoclickSettingsObserver() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         mController.onDestroy();
 
@@ -279,21 +279,61 @@
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void onDestroy_flagOn_clearAutoclickIndicatorScheduler() {
-        injectFakeMouseActionDownEvent();
+        injectFakeMouseActionHoverMoveEvent();
 
         mController.onDestroy();
 
         assertThat(mController.mAutoclickIndicatorScheduler).isNull();
     }
 
-    private void injectFakeMouseActionDownEvent() {
-        MotionEvent event = getFakeMotionDownEvent();
+    @Test
+    @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+    public void onMotionEvent_hoverEnter_doesNotScheduleClick() {
+        injectFakeMouseActionHoverMoveEvent();
+
+        // Send hover enter event.
+        MotionEvent hoverEnter = MotionEvent.obtain(
+                /* downTime= */ 0,
+                /* eventTime= */ 100,
+                /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+                /* x= */ 30f,
+                /* y= */ 0f,
+                /* metaState= */ 0);
+        hoverEnter.setSource(InputDevice.SOURCE_MOUSE);
+        mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0);
+
+        // Verify there is no pending click.
+        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
+    }
+
+    @Test
+    @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+    public void onMotionEvent_hoverMove_scheduleClick() {
+        injectFakeMouseActionHoverMoveEvent();
+
+        // Send hover move event.
+        MotionEvent hoverMove = MotionEvent.obtain(
+                /* downTime= */ 0,
+                /* eventTime= */ 100,
+                /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+                /* x= */ 30f,
+                /* y= */ 0f,
+                /* metaState= */ 0);
+        hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+        mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+
+        // Verify there is a pending click.
+        assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
+    }
+
+    private void injectFakeMouseActionHoverMoveEvent() {
+        MotionEvent event = getFakeMotionHoverMoveEvent();
         event.setSource(InputDevice.SOURCE_MOUSE);
         mController.onMotionEvent(event, event, /* policyFlags= */ 0);
     }
 
-    private void injectFakeNonMouseActionDownEvent() {
-        MotionEvent event = getFakeMotionDownEvent();
+    private void injectFakeNonMouseActionHoverMoveEvent() {
+        MotionEvent event = getFakeMotionHoverMoveEvent();
         event.setSource(InputDevice.SOURCE_KEYBOARD);
         mController.onMotionEvent(event, event, /* policyFlags= */ 0);
     }
@@ -309,11 +349,11 @@
         mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
     }
 
-    private MotionEvent getFakeMotionDownEvent() {
+    private MotionEvent getFakeMotionHoverMoveEvent() {
         return MotionEvent.obtain(
                 /* downTime= */ 0,
                 /* eventTime= */ 0,
-                /* action= */ MotionEvent.ACTION_DOWN,
+                /* action= */ MotionEvent.ACTION_HOVER_MOVE,
                 /* x= */ 0,
                 /* y= */ 0,
                 /* metaState= */ 0);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
index f033459..00cc726 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
@@ -18,9 +18,15 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
+import android.graphics.drawable.GradientDrawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
 import android.testing.TestableLooper;
@@ -28,6 +34,8 @@
 import android.view.WindowManager;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.R;
 
 import org.junit.Before;
@@ -56,11 +64,25 @@
     private LinearLayout mDragButton;
     private LinearLayout mScrollButton;
 
+    private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK;
+
+    private final ClickPanelControllerInterface clickPanelController =
+            new ClickPanelControllerInterface() {
+                @Override
+                public void handleAutoclickTypeChange(@AutoclickType int clickType) {
+                    mActiveClickType = clickType;
+                }
+
+                @Override
+                public void toggleAutoclickPause() {}
+            };
+
     @Before
     public void setUp() {
         mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
 
-        mAutoclickTypePanel = new AutoclickTypePanel(mTestableContext, mMockWindowManager);
+        mAutoclickTypePanel =
+                new AutoclickTypePanel(mTestableContext, mMockWindowManager, clickPanelController);
         View contentView = mAutoclickTypePanel.getContentViewForTesting();
         mLeftClickButton = contentView.findViewById(R.id.accessibility_autoclick_left_click_layout);
         mRightClickButton =
@@ -87,6 +109,11 @@
     }
 
     @Test
+    public void AutoclickTypePanel_initialState_correctButtonStyle() {
+        verifyButtonHasSelectedStyle(mLeftClickButton);
+    }
+
+    @Test
     public void togglePanelExpansion_onClick_expandedTrue() {
         // On clicking left click button, the panel is expanded and all buttons are visible.
         mLeftClickButton.callOnClick();
@@ -116,4 +143,32 @@
         assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE);
         assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE);
     }
+
+    @Test
+    public void togglePanelExpansion_selectButton_correctStyle() {
+        // By first click, the panel is expanded.
+        mLeftClickButton.callOnClick();
+
+        // Clicks any button in the expanded state to select a type button.
+        mScrollButton.callOnClick();
+
+        verifyButtonHasSelectedStyle(mScrollButton);
+    }
+
+    @Test
+    public void togglePanelExpansion_selectButton_correctActiveClickType() {
+        // By first click, the panel is expanded.
+        mLeftClickButton.callOnClick();
+
+        // Clicks any button in the expanded state to select a type button.
+        mScrollButton.callOnClick();
+
+        assertThat(mActiveClickType).isEqualTo(AUTOCLICK_TYPE_SCROLL);
+    }
+
+    private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) {
+        GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+        assertThat(gradientDrawable.getColor().getDefaultColor())
+                .isEqualTo(mTestableContext.getColor(R.color.materialColorPrimary));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS
new file mode 100644
index 0000000..9592bfd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 770744.
+
+include /services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 1627f68..06958b8 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -25,7 +25,6 @@
 import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
 import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
 import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
 
@@ -116,6 +115,7 @@
 import com.android.server.pm.UserManagerService;
 import com.android.server.pm.UserTypeDetails;
 import com.android.server.pm.UserTypeFactory;
+import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerService;
 
 import com.google.common.collect.Range;
@@ -1563,11 +1563,11 @@
         // and the thread is still alive
         assertTrue(threadStartUser.isAlive());
 
-        // mock the binder response for the user switch completion
-        ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
-        verify(mInjector.mWindowManagerMock).lockNow(captor.capture());
-        IRemoteCallback.Stub.asInterface(captor.getValue().getBinder(
-                LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null);
+        // mock send the keyguard shown event
+        ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
+                ActivityTaskManagerInternal.ScreenObserver.class);
+        verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
+        captor.getValue().onKeyguardStateChanged(true);
 
         // verify the switch now moves on...
         Thread.sleep(1000);
@@ -1757,6 +1757,7 @@
         private final IStorageManager mStorageManagerMock;
         private final UserManagerInternal mUserManagerInternalMock;
         private final WindowManagerService mWindowManagerMock;
+        private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
         private final PowerManagerInternal mPowerManagerInternal;
         private final AlarmManagerInternal mAlarmManagerInternal;
         private final KeyguardManager mKeyguardManagerMock;
@@ -1778,6 +1779,7 @@
             mUserManagerMock = mock(UserManagerService.class);
             mUserManagerInternalMock = mock(UserManagerInternal.class);
             mWindowManagerMock = mock(WindowManagerService.class);
+            mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class);
             mStorageManagerMock = mock(IStorageManager.class);
             mPowerManagerInternal = mock(PowerManagerInternal.class);
             mAlarmManagerInternal = mock(AlarmManagerInternal.class);
@@ -1841,6 +1843,11 @@
         }
 
         @Override
+        ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+            return mActivityTaskManagerInternal;
+        }
+
+        @Override
         PowerManagerInternal getPowerManagerInternal() {
             return mPowerManagerInternal;
         }
diff --git a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
index 17f5ebb..7349c5f4 100644
--- a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
@@ -116,6 +116,10 @@
         deleteFolder(InstrumentationRegistry.getTargetContext().getFilesDir());
     }
 
+    static {
+        System.loadLibrary("servicestestjni");
+    }
+
     @Test
     public void testReadGameServiceSettings() {
         writeOldFiles();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index ffcb961..ab7b4da 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -73,6 +73,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.hardware.Sensor;
+import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.IDisplayManager;
@@ -173,8 +174,7 @@
     private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
     private static final int VIRTUAL_DEVICE_ID_1 = 42;
     private static final int VIRTUAL_DEVICE_ID_2 = 43;
-    private static final VirtualDisplayConfig VIRTUAL_DISPLAY_CONFIG =
-            new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400).build();
+
     private static final VirtualDpadConfig DPAD_CONFIG =
             new VirtualDpadConfig.Builder()
                     .setVendorId(VENDOR_ID)
@@ -284,7 +284,12 @@
     private Intent createRestrictedActivityBlockedIntent(Set<String> displayCategories,
             String targetDisplayCategory) {
         when(mDisplayManagerInternalMock.createVirtualDisplay(any(), any(), any(), any(),
-                eq(VIRTUAL_DEVICE_OWNER_PACKAGE))).thenReturn(DISPLAY_ID_1);
+                eq(VIRTUAL_DEVICE_OWNER_PACKAGE)))
+                .thenAnswer(inv -> {
+                    mLocalService.onVirtualDisplayCreated(
+                            mDeviceImpl, DISPLAY_ID_1, inv.getArgument(1), inv.getArgument(3));
+                    return DISPLAY_ID_1;
+                });
         VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("display", 640, 480,
                 420).setDisplayCategories(displayCategories).build();
         mDeviceImpl.createVirtualDisplay(config, mVirtualDisplayCallback);
@@ -997,8 +1002,7 @@
     public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
             throws RemoteException {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
-        assertThrows(IllegalStateException.class,
-                () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         TestableLooper.get(this).processAllMessages();
         verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
                 nullable(String.class), nullable(String.class), nullable(WorkSource.class),
@@ -1871,8 +1875,6 @@
     }
 
     private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
-        when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
-                eq(virtualDevice), any(), any())).thenReturn(displayId);
         final String uniqueId = UNIQUE_ID + displayId;
         doAnswer(inv -> {
             final DisplayInfo displayInfo = new DisplayInfo();
@@ -1880,7 +1882,22 @@
             displayInfo.flags = flags;
             return displayInfo;
         }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
-        virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
+
+        when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
+                eq(virtualDevice), any(), any())).thenAnswer(inv -> {
+                    mLocalService.onVirtualDisplayCreated(
+                            virtualDevice, displayId, mVirtualDisplayCallback, inv.getArgument(3));
+                    return displayId;
+                });
+
+        final int virtualDisplayFlags = (flags & Display.FLAG_TRUSTED) == 0
+                ? 0
+                : DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+        VirtualDisplayConfig virtualDisplayConfig =
+                new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400)
+                        .setFlags(virtualDisplayFlags)
+                        .build();
+        virtualDevice.createVirtualDisplay(virtualDisplayConfig, mVirtualDisplayCallback);
         mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index 04d0752..a4e77c0 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -20,10 +20,12 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.hardware.contexthub.EndpointInfo;
 import android.hardware.contexthub.ErrorCode;
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier;
@@ -32,6 +34,7 @@
 import android.hardware.contexthub.IContextHubEndpointCallback;
 import android.hardware.contexthub.IEndpointCommunication;
 import android.hardware.contexthub.MessageDeliveryStatus;
+import android.hardware.contexthub.Reason;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
@@ -61,6 +64,9 @@
     private static final int ENDPOINT_ID = 1;
     private static final String ENDPOINT_PACKAGE_NAME = "com.android.server.location.contexthub";
 
+    private static final String TARGET_ENDPOINT_NAME = "Example target endpoint";
+    private static final int TARGET_ENDPOINT_ID = 1;
+
     private ContextHubClientManager mClientManager;
     private ContextHubEndpointManager mEndpointManager;
     private HubInfoRegistry mHubInfoRegistry;
@@ -95,23 +101,8 @@
 
     @Test
     public void testRegisterEndpoint() throws RemoteException {
-        // Register an endpoint and confirm we can get a valid IContextHubEndoint reference
-        HubEndpointInfo info =
-                new HubEndpointInfo(
-                        ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList());
-        IContextHubEndpoint endpoint =
-                mEndpointManager.registerEndpoint(
-                        info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null);
-        assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1);
-        assertThat(endpoint).isNotNull();
-        HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo();
-        assertThat(assignedInfo).isNotNull();
-        HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier();
-        assertThat(assignedIdentifier).isNotNull();
-
-        // Unregister the endpoint and confirm proper clean-up
-        mEndpointManager.unregisterEndpoint(assignedIdentifier.getEndpoint());
-        assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0);
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        unregisterExampleEndpoint(endpoint);
     }
 
     @Test
@@ -146,4 +137,107 @@
         assertThat(statusCaptor.getValue().messageSequenceNumber).isEqualTo(sequenceNumber);
         assertThat(statusCaptor.getValue().errorCode).isEqualTo(ErrorCode.DESTINATION_NOT_FOUND);
     }
+
+    @Test
+    public void testHalRestart() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+
+        // Verify that the endpoint is still registered after a HAL restart
+        HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo();
+        HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier();
+        mEndpointManager.onHalRestart();
+        ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class);
+        verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture());
+        assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint());
+        assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub());
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
+    @Test
+    public void testHalRestartOnOpenSession() throws RemoteException {
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+
+        HubEndpointInfo targetInfo =
+                new HubEndpointInfo(
+                        TARGET_ENDPOINT_NAME,
+                        TARGET_ENDPOINT_ID,
+                        ENDPOINT_PACKAGE_NAME,
+                        Collections.emptyList());
+        int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+        mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1);
+
+        mEndpointManager.onHalRestart();
+
+        HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo();
+        HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier();
+        ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class);
+        verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture());
+        assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint());
+        assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub());
+
+        verify(mMockCallback)
+                .onSessionClosed(
+                        sessionId, ContextHubServiceUtil.toAppHubEndpointReason(Reason.HUB_RESET));
+
+        unregisterExampleEndpoint(endpoint);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+    }
+
+    @Test
+    public void testOpenSessionOnUnregistration() throws RemoteException {
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+
+        HubEndpointInfo targetInfo =
+                new HubEndpointInfo(
+                        TARGET_ENDPOINT_NAME,
+                        TARGET_ENDPOINT_ID,
+                        ENDPOINT_PACKAGE_NAME,
+                        Collections.emptyList());
+        int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+        mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1);
+
+        unregisterExampleEndpoint(endpoint);
+        verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.ENDPOINT_GONE);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+    }
+
+    private IContextHubEndpoint registerExampleEndpoint() throws RemoteException {
+        HubEndpointInfo info =
+                new HubEndpointInfo(
+                        ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList());
+        IContextHubEndpoint endpoint =
+                mEndpointManager.registerEndpoint(
+                        info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null);
+        assertThat(endpoint).isNotNull();
+        HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo();
+        assertThat(assignedInfo).isNotNull();
+        HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier();
+        assertThat(assignedIdentifier).isNotNull();
+
+        // Confirm registerEndpoint was called with the right contents
+        ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class);
+        verify(mMockEndpointCommunications).registerEndpoint(statusCaptor.capture());
+        assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint());
+        assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub());
+        assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1);
+
+        return endpoint;
+    }
+
+    private void unregisterExampleEndpoint(IContextHubEndpoint endpoint) throws RemoteException {
+        HubEndpointInfo expectedInfo = endpoint.getAssignedHubEndpointInfo();
+        endpoint.unregister();
+        ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class);
+        verify(mMockEndpointCommunications).unregisterEndpoint(statusCaptor.capture());
+        assertThat(statusCaptor.getValue().id.id)
+                .isEqualTo(expectedInfo.getIdentifier().getEndpoint());
+        assertThat(statusCaptor.getValue().id.hubId)
+                .isEqualTo(expectedInfo.getIdentifier().getHub());
+        assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0);
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
index 770712a..4101192 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
@@ -204,7 +204,7 @@
                 .build());
         final Notification n = new Notification.Builder(getContext())
                 .setContentTitle("foo")
-                .setCategory(CATEGORY_ALARM)
+                .setCategory(new String("alarm"))
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .build();
         NotificationRecord r = getRecord(channel, n);
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index c6b431c..8e2cea7 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -402,9 +402,6 @@
 
     @Test
     @EnableFlags({com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SHORTCUT_CONTROL,
-            com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG,
-            com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG,
-            com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
             com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS})
     @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testKeyboardAccessibilityToggleShortcutPress() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 65150e7..bb29614 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -49,17 +49,13 @@
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.os.Process.NOBODY_UID;
 import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsSource.ID_IME;
-import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
-import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_PIP;
@@ -125,7 +121,6 @@
 import android.app.servertransaction.ClientTransactionItem;
 import android.app.servertransaction.DestroyActivityItem;
 import android.app.servertransaction.PauseActivityItem;
-import android.app.servertransaction.WindowStateResizeItem;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -149,8 +144,6 @@
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner.Stub;
 import android.view.IWindowManager;
-import android.view.InsetsSource;
-import android.view.InsetsState;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.Surface;
@@ -171,7 +164,6 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.invocation.InvocationOnMock;
 
 import java.util.ArrayList;
@@ -2889,7 +2881,6 @@
         activity2.addStartingWindow(mPackageName, android.R.style.Theme, activity1, true, true,
                 false, true, false, false, false);
         waitUntilHandlersIdle();
-        assertFalse(mDisplayContent.mSkipAppTransitionAnimation);
         assertNoStartingWindow(activity1);
         assertHasStartingWindow(activity2);
     }
@@ -2973,7 +2964,6 @@
                 false /* newTask */, false /* isTaskSwitch */, null /* options */,
                 null /* sourceRecord */);
 
-        assertTrue(mDisplayContent.mSkipAppTransitionAnimation);
         assertNull(middle.mStartingWindow);
         assertHasStartingWindow(top);
         assertTrue(top.isVisible());
@@ -3273,26 +3263,6 @@
                 > activity.getConfiguration().windowConfiguration.getAppBounds().height());
     }
 
-    @Test
-    public void testSetVisibility_visibleToVisible() {
-        final ActivityRecord activity = new ActivityBuilder(mAtm)
-                .setCreateTask(true).build();
-        // By default, activity is visible.
-        assertTrue(activity.isVisible());
-        assertTrue(activity.isVisibleRequested());
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
-
-        // Request the activity to be visible. Although the activity is already visible, app
-        // transition animation should be applied on this activity. This might be unnecessary, but
-        // until we verify no logic relies on this behavior, we'll keep this as is.
-        mDisplayContent.prepareAppTransition(0);
-        activity.setVisibility(true);
-        assertTrue(activity.isVisible());
-        assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
-    }
-
     @SetupWindows(addWindows = W_ACTIVITY)
     @Test
     public void testSetVisibility_visibleToInvisible() {
@@ -3324,222 +3294,30 @@
     public void testSetVisibility_invisibleToVisible() {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true).setVisible(false).build();
-        // Activiby is invisible. However ATMS requests it to become visible, since this is a top
-        // activity.
         assertFalse(activity.isVisible());
-        assertTrue(activity.isVisibleRequested());
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+        assertFalse(activity.isVisibleRequested());
 
         // Request the activity to be visible. Since the visibility changes, app transition
         // animation should be applied on this activity.
-        activity.setVisibility(true);
+        requestTransition(activity, WindowManager.TRANSIT_OPEN);
+        mWm.mRoot.resumeFocusedTasksTopActivities();
         assertFalse(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
-
-        // There should still be animation (add to opening) if keyguard is going away while the
-        // screen is off because it will be visible after screen is turned on by unlocking.
-        mDisplayContent.mOpeningApps.remove(activity);
-        mDisplayContent.mClosingApps.remove(activity);
-        activity.commitVisibility(false /* visible */, false /* performLayout */);
-        mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */);
-        final KeyguardController controller = mSupervisor.getKeyguardController();
-        doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
-        activity.setVisibility(true);
-        assertTrue(mDisplayContent.mOpeningApps.contains(activity));
+        assertTrue(activity.inTransition());
     }
 
     @Test
     public void testSetVisibility_invisibleToInvisible() {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true).setVisible(false).build();
-        // Activiby is invisible. However ATMS requests it to become visible, since this is a top
-        // activity.
-        assertFalse(activity.isVisible());
-        assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+        requestTransition(activity, WindowManager.TRANSIT_CLOSE);
 
         // Request the activity to be invisible. Since the activity is already invisible, no app
         // transition should be applied on this activity.
         activity.setVisibility(false);
         assertFalse(activity.isVisible());
         assertFalse(activity.isVisibleRequested());
-        assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
-    }
-
-    @SetupWindows(addWindows = W_INPUT_METHOD)
-    @Test
-    public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() {
-        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
-        makeWindowVisibleAndDrawn(app, mImeWindow);
-        mDisplayContent.setImeLayeringTarget(app);
-        mDisplayContent.setImeInputTarget(app);
-
-        // Simulate app is closing and expect the last IME is shown and IME insets is frozen.
-        mDisplayContent.mOpeningApps.clear();
-        app.mActivityRecord.commitVisibility(false, false);
-        app.mActivityRecord.onWindowsGone();
-
-        assertTrue(app.mActivityRecord.mLastImeShown);
-        assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
-        // Expect IME insets frozen state will reset when the activity has no IME focusable window.
-        app.mActivityRecord.forAllWindows(w -> {
-            w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
-            return true;
-        }, true);
-
-        app.mActivityRecord.commitVisibility(true, false);
-        app.mActivityRecord.onWindowsVisible();
-
-        assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-    }
-
-    @SetupWindows(addWindows = W_INPUT_METHOD)
-    @Test
-    public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() {
-        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
-
-        mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
-                mImeWindow, null, null);
-        mImeWindow.getControllableInsetProvider().setServerVisible(true);
-
-        InsetsSource imeSource = new InsetsSource(ID_IME, ime());
-        app.mAboveInsetsState.addSource(imeSource);
-        mDisplayContent.setImeLayeringTarget(app);
-        mDisplayContent.updateImeInputAndControlTarget(app);
-
-        InsetsState state = app.getInsetsState();
-        assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible());
-        assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty());
-
-        // Simulate app is closing and expect IME insets is frozen.
-        mDisplayContent.mOpeningApps.clear();
-        app.mActivityRecord.commitVisibility(false, false);
-        app.mActivityRecord.onWindowsGone();
-        assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
-        // Simulate app re-start input or turning screen off/on then unlocked by un-secure
-        // keyguard to back to the app, expect IME insets is not frozen
-        app.mActivityRecord.commitVisibility(true, false);
-        mDisplayContent.updateImeInputAndControlTarget(app);
-        performSurfacePlacementAndWaitForWindowAnimator();
-
-        assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
-        imeSource.setVisible(true);
-        imeSource.setFrame(new Rect(100, 400, 500, 500));
-        app.mAboveInsetsState.addSource(imeSource);
-
-        // Verify when IME is visible and the app can receive the right IME insets from policy.
-        makeWindowVisibleAndDrawn(app, mImeWindow);
-        state = app.getInsetsState();
-        assertTrue(state.peekSource(ID_IME).isVisible());
-        assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame());
-    }
-
-    @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
-    @Test
-    public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest()
-            throws RemoteException {
-        final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build();
-        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
-
-        mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
-                mImeWindow, null, null);
-        mImeWindow.getControllableInsetProvider().setServerVisible(true);
-
-        // Simulate app2 is closing and let app1 is visible to be IME targets.
-        makeWindowVisibleAndDrawn(app1, mImeWindow);
-        mDisplayContent.setImeLayeringTarget(app1);
-        mDisplayContent.updateImeInputAndControlTarget(app1);
-        app2.mActivityRecord.commitVisibility(false, false);
-
-        // app1 requests IME visible.
-        app1.setRequestedVisibleTypes(ime(), ime());
-        mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1,
-                null /* statsToken */);
-
-        // Verify app1's IME insets is visible and app2's IME insets frozen flag set.
-        assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
-        assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
-        // Simulate switching to app2 to make it visible to be IME targets.
-        spyOn(app2);
-        spyOn(app2.mClient);
-        spyOn(app2.getProcess());
-        ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class);
-        doReturn(true).when(app2).isReadyToDispatchInsetsState();
-        mDisplayContent.setImeLayeringTarget(app2);
-        app2.mActivityRecord.commitVisibility(true, false);
-        mDisplayContent.updateImeInputAndControlTarget(app2);
-        performSurfacePlacementAndWaitForWindowAnimator();
-
-        // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets
-        // to client if the app didn't request IME visible.
-        assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
-        verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
-                isA(WindowStateResizeItem.class));
-        assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
-    }
-
-    @Test
-    public void testImeInsetsFrozenFlag_multiWindowActivities() {
-        final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent);
-        final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken(
-                imeToken).build();
-        makeWindowVisibleAndDrawn(ime);
-
-        // Create a split-screen root task with activity1 and activity 2.
-        final Task task = new TaskBuilder(mSupervisor)
-                .setCreateParentTask(true).setCreateActivity(true).build();
-        task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        final ActivityRecord activity1 = task.getTopNonFinishingActivity();
-        activity1.getTask().setResumedActivity(activity1, "testApp1");
-
-        final ActivityRecord activity2 = new TaskBuilder(mSupervisor)
-                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
-                .setCreateActivity(true).build().getTopMostActivity();
-        activity2.getTask().setResumedActivity(activity2, "testApp2");
-        activity2.getTask().setParent(task.getRootTask());
-
-        // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when
-        // invisible to user.
-        activity1.mImeInsetsFrozenUntilStartInput = true;
-        activity2.mImeInsetsFrozenUntilStartInput = true;
-
-        final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken(
-                activity1).build();
-        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken(
-                activity2).build();
-        makeWindowVisibleAndDrawn(app1, app2);
-
-        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
-        controller.getImeSourceProvider().setWindowContainer(
-                ime, null, null);
-        ime.getControllableInsetProvider().setServerVisible(true);
-
-        // app1 starts input and expect IME insets for all activities in split-screen will be
-        // frozen until the input started.
-        mDisplayContent.setImeLayeringTarget(app1);
-        mDisplayContent.updateImeInputAndControlTarget(app1);
-        mDisplayContent.computeImeTarget(true /* updateImeTarget */);
-        performSurfacePlacementAndWaitForWindowAnimator();
-
-        assertEquals(app1, mDisplayContent.getImeInputTarget());
-        assertFalse(activity1.mImeInsetsFrozenUntilStartInput);
-        assertFalse(activity2.mImeInsetsFrozenUntilStartInput);
-
-        app1.setRequestedVisibleTypes(ime());
-        controller.onRequestedVisibleTypesChanged(app1, null /* statsToken */);
-
-        // Expect all activities in split-screen will get IME insets visible state
-        assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
-        assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible());
+        assertFalse(activity.inTransition());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java b/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java
deleted file mode 100644
index 8871056..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for the {@link ActivityStack} class.
- *
- * Build/Install/Run:
- *  atest WmTests:AnimatingActivityRegistryTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AnimatingActivityRegistryTest extends WindowTestsBase {
-
-    @Mock
-    AnimationAdapter mAdapter;
-
-    @Mock
-    Runnable mMockEndDeferFinishCallback1;
-    @Mock
-    Runnable mMockEndDeferFinishCallback2;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testDeferring() {
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        final ActivityRecord activity2 = createAppWindow(activity1.getTask(), ACTIVITY_TYPE_STANDARD,
-                "activity2").mActivityRecord;
-        final AnimatingActivityRegistry registry =
-                activity1.getRootTask().getAnimatingActivityRegistry();
-
-        activity1.startAnimation(activity1.getPendingTransaction(), mAdapter, false /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        activity2.startAnimation(activity1.getPendingTransaction(), mAdapter, false /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        assertTrue(activity1.isAnimating(TRANSITION));
-        assertTrue(activity2.isAnimating(TRANSITION));
-
-        // Make sure that first animation finish is deferred, second one is not deferred, and first
-        // one gets cancelled.
-        assertTrue(registry.notifyAboutToFinish(activity1, mMockEndDeferFinishCallback1));
-        assertFalse(registry.notifyAboutToFinish(activity2, mMockEndDeferFinishCallback2));
-        verify(mMockEndDeferFinishCallback1).run();
-        verifyZeroInteractions(mMockEndDeferFinishCallback2);
-    }
-
-    @Test
-    public void testContainerRemoved() {
-        final ActivityRecord window1 = createActivityRecord(mDisplayContent);
-        final ActivityRecord window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD,
-                "window2").mActivityRecord;
-        final AnimatingActivityRegistry registry =
-                window1.getRootTask().getAnimatingActivityRegistry();
-
-        window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        assertTrue(window1.isAnimating(TRANSITION));
-        assertTrue(window2.isAnimating(TRANSITION));
-
-        // Make sure that first animation finish is deferred, and removing the second window stops
-        // finishes all pending deferred finishings.
-        registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1);
-        window2.setParent(null);
-        verify(mMockEndDeferFinishCallback1).run();
-    }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
deleted file mode 100644
index c294bc6..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ /dev/null
@@ -1,1306 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.Nullable;
-import android.graphics.Rect;
-import android.gui.DropInputMode;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-import android.util.ArraySet;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationDefinition;
-import android.view.RemoteAnimationTarget;
-import android.view.WindowManager;
-import android.window.ITaskFragmentOrganizer;
-import android.window.TaskFragmentOrganizer;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Build/Install/Run:
- *  atest WmTests:AppTransitionControllerTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AppTransitionControllerTest extends WindowTestsBase {
-
-    private AppTransitionController mAppTransitionController;
-
-    @Before
-    public void setUp() throws Exception {
-        assumeFalse(WindowManagerService.sEnableShellTransitions);
-        mAppTransitionController = new AppTransitionController(mWm, mDisplayContent);
-        mWm.mAnimator.ready();
-    }
-
-    @Test
-    public void testSkipOccludedActivityCloseTransition() {
-        final ActivityRecord behind = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final ActivityRecord topOpening = createActivityRecord(behind.getTask());
-        topOpening.setOccludesParent(true);
-        topOpening.setVisible(true);
-
-        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
-        mDisplayContent.mClosingApps.add(behind);
-
-        assertEquals(WindowManager.TRANSIT_OLD_UNSET,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testClearTaskSkipAppExecuteTransition() {
-        final ActivityRecord behind = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final Task task = behind.getTask();
-        final ActivityRecord top = createActivityRecord(task);
-        top.setState(ActivityRecord.State.RESUMED, "test");
-        behind.setState(ActivityRecord.State.STARTED, "test");
-        behind.setVisibleRequested(true);
-
-        task.removeActivities("test", false /* excludingTaskOverlay */);
-        assertFalse(mDisplayContent.mAppTransition.isReady());
-    }
-
-    @Test
-    public void testTranslucentOpen() {
-        final ActivityRecord behind = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final ActivityRecord translucentOpening = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        doReturn(false).when(translucentOpening).fillsParent();
-        translucentOpening.setVisible(false);
-        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
-        mDisplayContent.mOpeningApps.add(behind);
-        mDisplayContent.mOpeningApps.add(translucentOpening);
-
-        assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testTranslucentClose() {
-        final ActivityRecord behind = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final ActivityRecord translucentClosing = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        doReturn(false).when(translucentClosing).fillsParent();
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
-        mDisplayContent.mClosingApps.add(translucentClosing);
-        assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testDreamActivityOpenTransition() {
-        final ActivityRecord dreamActivity = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM);
-        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
-        mDisplayContent.mOpeningApps.add(dreamActivity);
-
-        assertEquals(TRANSIT_OLD_DREAM_ACTIVITY_OPEN,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testDreamActivityCloseTransition() {
-        final ActivityRecord dreamActivity = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM);
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
-        mDisplayContent.mClosingApps.add(dreamActivity);
-
-        assertEquals(TRANSIT_OLD_DREAM_ACTIVITY_CLOSE,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testChangeIsNotOverwritten() {
-        final ActivityRecord behind = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final ActivityRecord translucentOpening = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        translucentOpening.setOccludesParent(false);
-        translucentOpening.setVisible(false);
-        mDisplayContent.prepareAppTransition(TRANSIT_CHANGE);
-        mDisplayContent.mOpeningApps.add(behind);
-        mDisplayContent.mOpeningApps.add(translucentOpening);
-        mDisplayContent.mChangingContainers.add(translucentOpening.getTask());
-        assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null, null, false));
-    }
-
-    @Test
-    public void testTransitWithinTask() {
-        final ActivityRecord opening = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
-        opening.setOccludesParent(false);
-        final ActivityRecord closing = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
-        closing.setOccludesParent(false);
-        final Task task = opening.getTask();
-        mDisplayContent.mOpeningApps.add(opening);
-        mDisplayContent.mClosingApps.add(closing);
-        assertFalse(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_ACTIVITY_OPEN, task));
-        closing.getTask().removeChild(closing);
-        task.addChild(closing, 0);
-        assertTrue(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_ACTIVITY_OPEN, task));
-        assertFalse(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_TASK_OPEN, task));
-    }
-
-
-    @Test
-    public void testIntraWallpaper_open() {
-        final ActivityRecord opening = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        opening.setVisible(false);
-        final WindowManager.LayoutParams attrOpening = new WindowManager.LayoutParams(
-                TYPE_BASE_APPLICATION);
-        attrOpening.setTitle("WallpaperOpening");
-        attrOpening.flags |= FLAG_SHOW_WALLPAPER;
-        final TestWindowState appWindowOpening = createWindowState(attrOpening, opening);
-        opening.addWindow(appWindowOpening);
-
-        final ActivityRecord closing = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final WindowManager.LayoutParams attrClosing = new WindowManager.LayoutParams(
-                TYPE_BASE_APPLICATION);
-        attrOpening.setTitle("WallpaperClosing");
-        attrClosing.flags |= FLAG_SHOW_WALLPAPER;
-        final TestWindowState appWindowClosing = createWindowState(attrClosing, closing);
-        closing.addWindow(appWindowClosing);
-
-        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
-        mDisplayContent.mOpeningApps.add(opening);
-        mDisplayContent.mClosingApps.add(closing);
-
-        assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, appWindowClosing, null, false));
-    }
-
-    @Test
-    public void testIntraWallpaper_toFront() {
-        final ActivityRecord opening = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        opening.setVisible(false);
-        final WindowManager.LayoutParams attrOpening = new WindowManager.LayoutParams(
-                TYPE_BASE_APPLICATION);
-        attrOpening.setTitle("WallpaperOpening");
-        attrOpening.flags |= FLAG_SHOW_WALLPAPER;
-        final TestWindowState appWindowOpening = createWindowState(attrOpening, opening);
-        opening.addWindow(appWindowOpening);
-
-        final ActivityRecord closing = createActivityRecord(mDisplayContent,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        final WindowManager.LayoutParams attrClosing = new WindowManager.LayoutParams(
-                TYPE_BASE_APPLICATION);
-        attrOpening.setTitle("WallpaperClosing");
-        attrClosing.flags |= FLAG_SHOW_WALLPAPER;
-        final TestWindowState appWindowClosing = createWindowState(attrClosing, closing);
-        closing.addWindow(appWindowClosing);
-
-        mDisplayContent.prepareAppTransition(TRANSIT_TO_FRONT);
-        mDisplayContent.mOpeningApps.add(opening);
-        mDisplayContent.mClosingApps.add(closing);
-
-        assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, appWindowClosing, null, false));
-    }
-
-    @Test
-    public void testGetAnimationTargets_visibilityAlreadyUpdated() {
-        // [DisplayContent] -+- [Task1] - [ActivityRecord1] (opening, visible)
-        //                   +- [Task2] - [ActivityRecord2] (closing, invisible)
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(false);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // No animation, since visibility of the opening and closing apps are already updated
-        // outside of AppTransition framework.
-        assertEquals(
-                new ArraySet<>(),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_visibilityAlreadyUpdated_butForcedTransitionRequested() {
-        // [DisplayContent] -+- [Task1] - [ActivityRecord1] (closing, invisible)
-        //                   +- [Task2] - [ActivityRecord2] (opening, visible)
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(true);
-        activity1.setVisibleRequested(true);
-        activity1.mRequestForceTransition = true;
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(false);
-        activity2.mRequestForceTransition = true;
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // The visibility are already updated, but since forced transition is requested, it will
-        // be included.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity2.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_exitingBeforeTransition() {
-        // Create another non-empty task so the animation target won't promote to task display area.
-        createActivityRecord(mDisplayContent);
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        activity.setVisible(false);
-        activity.mIsExiting = true;
-
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity);
-
-        // Animate closing apps even if it's not visible when it is exiting before we had a chance
-        // to play the transition animation.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        new ArraySet<>(), closing, false /* visible */));
-    }
-
-    @Test
-    public void testExitAnimationDone_beforeAppTransition() {
-        final Task task = createTask(mDisplayContent);
-        final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "Win");
-        spyOn(win);
-        win.mAnimatingExit = true;
-        mDisplayContent.mAppTransition.setTimeout();
-        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
-
-        verify(win).onExitAnimationDone();
-    }
-
-    @Test
-    public void testGetAnimationTargets_openingClosingInDifferentTask() {
-        // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible)
-        //                   |           +- [ActivityRecord2] (invisible)
-        //                   |
-        //                   +- [Task2] -+- [ActivityRecord3] (closing, visible)
-        //                               +- [ActivityRecord4] (invisible)
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
-                activity1.getTask());
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(false);
-
-        final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
-        final ActivityRecord activity4 = createActivityRecord(mDisplayContent,
-                activity3.getTask());
-        activity4.setVisible(false);
-        activity4.setVisibleRequested(false);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity3);
-
-        // Promote animation targets to root Task level. Invisible ActivityRecords don't affect
-        // promotion decision.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity3.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_openingClosingInSameTask() {
-        // [DisplayContent] - [Task] -+- [ActivityRecord1] (opening, invisible)
-        //                            +- [ActivityRecord2] (closing, visible)
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
-                activity1.getTask());
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // Don't promote an animation target to Task level, since the same task contains both
-        // opening and closing app.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity2}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_animateOnlyTranslucentApp() {
-        // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible)
-        //                   |           +- [ActivityRecord2] (visible)
-        //                   |
-        //                   +- [Task2] -+- [ActivityRecord3] (closing, visible)
-        //                               +- [ActivityRecord4] (visible)
-
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        activity1.setOccludesParent(false);
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
-                activity1.getTask());
-
-        final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
-        activity3.setOccludesParent(false);
-        final ActivityRecord activity4 = createActivityRecord(mDisplayContent,
-                activity3.getTask());
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity3);
-
-        // Don't promote an animation target to Task level, since opening (closing) app is
-        // translucent and is displayed over other non-animating app.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity3}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_animateTranslucentAndOpaqueApps() {
-        // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible)
-        //                   |           +- [ActivityRecord2] (opening, invisible)
-        //                   |
-        //                   +- [Task2] -+- [ActivityRecord3] (closing, visible)
-        //                               +- [ActivityRecord4] (closing, visible)
-
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        activity1.setOccludesParent(false);
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
-                activity1.getTask());
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(true);
-
-        final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
-        activity3.setOccludesParent(false);
-        final ActivityRecord activity4 = createActivityRecord(mDisplayContent,
-                activity3.getTask());
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        opening.add(activity2);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity3);
-        closing.add(activity4);
-
-        // Promote animation targets to TaskStack level even though opening (closing) app is
-        // translucent as long as all visible siblings animate at the same time.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity3.getRootTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_taskContainsMultipleTasks() {
-        // [DisplayContent] - [Task] -+- [Task1] - [ActivityRecord1] (opening, invisible)
-        //                            +- [Task2] - [ActivityRecord2] (closing, visible)
-        final Task parentTask = createTask(mDisplayContent);
-        final ActivityRecord activity1 = createActivityRecordWithParentTask(parentTask);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        final ActivityRecord activity2 = createActivityRecordWithParentTask(parentTask);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // Promote animation targets up to Task level, not beyond.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1.getTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity2.getTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_splitScreenOpening() {
-        // [DisplayContent] - [Task] -+- [split task 1] -+- [Task1] - [AR1] (opening, invisible)
-        //                            +- [split task 2] -+- [Task2] - [AR2] (opening, invisible)
-        final Task singleTopRoot = createTask(mDisplayContent);
-        final TaskBuilder builder = new TaskBuilder(mSupervisor)
-                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
-                .setParentTask(singleTopRoot)
-                .setCreatedByOrganizer(true);
-        final Task splitRoot1 = builder.build();
-        final Task splitRoot2 = builder.build();
-        splitRoot1.setAdjacentTaskFragment(splitRoot2);
-        final ActivityRecord activity1 = createActivityRecordWithParentTask(splitRoot1);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        final ActivityRecord activity2 = createActivityRecordWithParentTask(splitRoot2);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(true);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        opening.add(activity2);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-
-        // Promote animation targets up to Task level, not beyond.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{splitRoot1, splitRoot2}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_openingClosingTaskFragment() {
-        // [DefaultTDA] - [Task] -+- [TaskFragment1] - [ActivityRecord1] (opening, invisible)
-        //                        +- [TaskFragment2] - [ActivityRecord2] (closing, visible)
-        final Task parentTask = createTask(mDisplayContent);
-        final TaskFragment taskFragment1 = createTaskFragmentWithActivity(parentTask);
-        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-
-        final TaskFragment taskFragment2 = createTaskFragmentWithActivity(parentTask);
-        final ActivityRecord activity2 = taskFragment2.getTopMostActivity();
-        activity2.setVisible(true);
-        activity2.setVisibleRequested(false);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // Promote animation targets up to TaskFragment level, not beyond.
-        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment2}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_openingTheOnlyTaskFragmentInTask() {
-        // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (opening, invisible)
-        //               +- [Task2] - [ActivityRecord2] (closing, visible)
-        final Task task1 = createTask(mDisplayContent);
-        final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
-        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
-        activity2.setVisible(true);
-        activity2.setVisibleRequested(false);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // Promote animation targets up to leaf Task level because there's only one TaskFragment in
-        // the Task.
-        assertEquals(new ArraySet<>(new WindowContainer[]{task1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_closingTheOnlyTaskFragmentInTask() {
-        // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (closing, visible)
-        //               +- [Task2] - [ActivityRecord2] (opening, invisible)
-        final Task task1 = createTask(mDisplayContent);
-        final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
-        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
-        activity1.setVisible(true);
-        activity1.setVisibleRequested(false);
-
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(true);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity2);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity1);
-
-        // Promote animation targets up to leaf Task level because there's only one TaskFragment in
-        // the Task.
-        assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(new ArraySet<>(new WindowContainer[]{task1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    @Test
-    public void testGetAnimationTargets_embeddedTask() {
-        // [DisplayContent] -+- [Task1] -            [ActivityRecord1] (opening, invisible)
-        //                   +- [Task2] (embedded) - [ActivityRecord2] (opening, invisible)
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-
-        final Task task2 = createTask(mDisplayContent);
-        task2.mRemoveWithTaskOrganizer = true;
-        final ActivityRecord activity2 = createActivityRecord(task2);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(true);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        opening.add(activity2);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-
-        // No animation on the embedded task.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1.getTask()}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-
-    @Test
-    public void testGetAnimationTargets_activityInEmbeddedTask() {
-        // [DisplayContent] - [Task] (embedded)-+- [ActivityRecord1] (opening, invisible)
-        //                                      +- [ActivityRecord2] (closing, visible)
-        final Task task = createTask(mDisplayContent);
-        task.mRemoveWithTaskOrganizer = true;
-
-        final ActivityRecord activity1 = createActivityRecord(task);
-        activity1.setVisible(false);
-        activity1.setVisibleRequested(true);
-        final ActivityRecord activity2 = createActivityRecord(task);
-
-        final ArraySet<ActivityRecord> opening = new ArraySet<>();
-        opening.add(activity1);
-        final ArraySet<ActivityRecord> closing = new ArraySet<>();
-        closing.add(activity2);
-
-        // Even though embedded task itself doesn't animate, activities in an embedded task
-        // animate.
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity1}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, true /* visible */));
-        assertEquals(
-                new ArraySet<>(new WindowContainer[]{activity2}),
-                AppTransitionController.getAnimationTargets(
-                        opening, closing, false /* visible */));
-    }
-
-    static class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
-        private IRemoteAnimationFinishedCallback mFinishedCallback;
-
-        @Override
-        public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
-                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
-                IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
-            mFinishedCallback = finishedCallback;
-        }
-
-        @Override
-        public void onAnimationCancelled() throws RemoteException {
-            mFinishedCallback = null;
-        }
-
-        @Override
-        public IBinder asBinder() {
-            return new Binder();
-        }
-
-        boolean isAnimationStarted() {
-            return mFinishedCallback != null;
-        }
-
-        void finishAnimation() {
-            try {
-                mFinishedCallback.onAnimationFinished();
-            } catch (RemoteException e) {
-                fail();
-            }
-        }
-    }
-
-    @Test
-    public void testGetRemoteAnimationOverrideEmpty() {
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        assertNull(mAppTransitionController.getRemoteAnimationOverride(activity,
-                TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-    }
-
-    @Test
-    public void testGetRemoteAnimationOverrideWindowContainer() {
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter);
-        activity.registerRemoteAnimations(definition);
-
-        assertEquals(adapter,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-        assertNull(mAppTransitionController.getRemoteAnimationOverride(
-                        null, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-    }
-
-    @Test
-    public void testGetRemoteAnimationOverrideTransitionController() {
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter);
-        mAppTransitionController.registerRemoteAnimations(definition);
-
-        assertEquals(adapter,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-        assertEquals(adapter,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        null, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-    }
-
-    @Test
-    public void testGetRemoteAnimationOverrideBoth() {
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        final RemoteAnimationDefinition definition1 = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter1 = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition1.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter1);
-        activity.registerRemoteAnimations(definition1);
-
-        final RemoteAnimationDefinition definition2 = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter2 = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition2.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_UNOCCLUDE, adapter2);
-        mAppTransitionController.registerRemoteAnimations(definition2);
-
-        assertEquals(adapter2,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        activity, TRANSIT_OLD_KEYGUARD_UNOCCLUDE, new ArraySet<Integer>()));
-        assertEquals(adapter2,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        null, TRANSIT_OLD_KEYGUARD_UNOCCLUDE, new ArraySet<Integer>()));
-    }
-
-    @Test
-    public void testGetRemoteAnimationOverrideWindowContainerHasPriority() {
-        final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        final RemoteAnimationDefinition definition1 = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter1 = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition1.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter1);
-        activity.registerRemoteAnimations(definition1);
-
-        final RemoteAnimationDefinition definition2 = new RemoteAnimationDefinition();
-        final RemoteAnimationAdapter adapter2 = new RemoteAnimationAdapter(
-                new TestRemoteAnimationRunner(), 10, 1);
-        definition2.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter2);
-        mAppTransitionController.registerRemoteAnimations(definition2);
-
-        assertEquals(adapter1,
-                mAppTransitionController.getRemoteAnimationOverride(
-                        activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_overrideWithEmbeddedActivity() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord activity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(activity);
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation run by the remote handler.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_noOverrideWithOnlyTaskFragmentFillingTask() {
-        final Task task = createTask(mDisplayContent);
-        final ActivityRecord closingActivity = createActivityRecord(task);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-
-        // Make sure the TaskFragment is not embedded.
-        assertFalse(taskFragment.isEmbeddedWithBoundsOverride());
-        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(closingActivity);
-        prepareActivityForAppTransition(openingActivity);
-        final int uid = 12345;
-        closingActivity.info.applicationInfo.uid = uid;
-        openingActivity.info.applicationInfo.uid = uid;
-        task.effectiveUid = uid;
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity,
-                null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation is not run by the remote handler because the activity is filling the Task.
-        assertFalse(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_overrideWithTaskFragmentNotFillingTask() {
-        final Task task = createTask(mDisplayContent);
-        final ActivityRecord closingActivity = createActivityRecord(task);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-
-        // Make sure the TaskFragment is embedded.
-        taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        final Rect embeddedBounds = new Rect(task.getBounds());
-        embeddedBounds.right = embeddedBounds.left + embeddedBounds.width() / 2;
-        taskFragment.setBounds(embeddedBounds);
-        assertTrue(taskFragment.isEmbeddedWithBoundsOverride());
-        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(closingActivity);
-        prepareActivityForAppTransition(openingActivity);
-        final int uid = 12345;
-        closingActivity.info.applicationInfo.uid = uid;
-        openingActivity.info.applicationInfo.uid = uid;
-        task.effectiveUid = uid;
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity,
-                null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation run by the remote handler.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Closing non-embedded activity.
-        final ActivityRecord closingActivity = createActivityRecord(task);
-        prepareActivityForAppTransition(closingActivity);
-        // Opening TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(openingActivity);
-        task.effectiveUid = openingActivity.getUid();
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation run by the remote handler.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_overrideEmbeddedActivityWithDiffUid() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Closing TaskFragment with embedded activity.
-        final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord closingActivity = taskFragment1.getTopMostActivity();
-        prepareActivityForAppTransition(closingActivity);
-        closingActivity.info.applicationInfo.uid = 12345;
-        // Opening TaskFragment with embedded activity with different UID.
-        final TaskFragment taskFragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord openingActivity = taskFragment2.getTopMostActivity();
-        prepareActivityForAppTransition(openingActivity);
-        openingActivity.info.applicationInfo.uid = 54321;
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation run by the remote handler.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_noOverrideWithTwoApps() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Closing activity in Task1.
-        final ActivityRecord closingActivity = createActivityRecord(mDisplayContent);
-        prepareActivityForAppTransition(closingActivity);
-        // Opening TaskFragment with embedded activity in Task2.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(openingActivity);
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation not run by the remote handler.
-        assertFalse(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_noOverrideNonEmbeddedActivityWithDiffUid() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Closing TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord closingActivity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(closingActivity);
-        closingActivity.info.applicationInfo.uid = 12345;
-        task.effectiveUid = closingActivity.getUid();
-        // Opening non-embedded activity with different UID.
-        final ActivityRecord openingActivity = createActivityRecord(task);
-        prepareActivityForAppTransition(openingActivity);
-        openingActivity.info.applicationInfo.uid = 54321;
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation should not run by the remote handler when there are non-embedded activities of
-        // different UID.
-        assertFalse(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_noOverrideWithWallpaper() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with embedded activity.
-        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final ActivityRecord activity = taskFragment.getTopMostActivity();
-        prepareActivityForAppTransition(activity);
-        // Set wallpaper as visible.
-        final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
-                mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */);
-        spyOn(mDisplayContent.mWallpaperController);
-        doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible();
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // Animation should not run by the remote handler when there is wallpaper in the transition.
-        assertFalse(remoteAnimationRunner.isAnimationStarted());
-    }
-
-    @Test
-    public void testOverrideTaskFragmentAdapter_inputProtectedForUntrustedAnimation() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with embedded activities, one is trusted embedded, and the other
-        // one is untrusted embedded.
-        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .createActivityCount(2)
-                .setOrganizer(organizer)
-                .build();
-        final ActivityRecord activity0 = taskFragment.getChildAt(0).asActivityRecord();
-        final ActivityRecord activity1 = taskFragment.getChildAt(1).asActivityRecord();
-        // Also create a non-embedded activity in the Task.
-        final ActivityRecord activity2 = new ActivityBuilder(mAtm).build();
-        task.addChild(activity2, POSITION_BOTTOM);
-        prepareActivityForAppTransition(activity0);
-        prepareActivityForAppTransition(activity1);
-        prepareActivityForAppTransition(activity2);
-        doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity0);
-        doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity1);
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // The animation will be animated remotely by client and all activities are input disabled
-        // for untrusted animation.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-        verify(activity0).setDropInputForAnimation(true);
-        verify(activity1).setDropInputForAnimation(true);
-        verify(activity2).setDropInputForAnimation(true);
-        verify(activity0).setDropInputMode(DropInputMode.ALL);
-        verify(activity1).setDropInputMode(DropInputMode.ALL);
-        verify(activity2).setDropInputMode(DropInputMode.ALL);
-
-        // Reset input after animation is finished.
-        clearInvocations(activity0);
-        clearInvocations(activity1);
-        clearInvocations(activity2);
-        remoteAnimationRunner.finishAnimation();
-
-        verify(activity0).setDropInputForAnimation(false);
-        verify(activity1).setDropInputForAnimation(false);
-        verify(activity2).setDropInputForAnimation(false);
-        verify(activity0).setDropInputMode(DropInputMode.OBSCURED);
-        verify(activity1).setDropInputMode(DropInputMode.NONE);
-        verify(activity2).setDropInputMode(DropInputMode.NONE);
-    }
-
-    /**
-     * Since we don't have any use case to rely on handling input during animation, disable it even
-     * if it is trusted embedding so that it could cover some edge-cases when a previously trusted
-     * host starts doing something bad.
-     */
-    @Test
-    public void testOverrideTaskFragmentAdapter_inputProtectedForTrustedAnimation() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with only trusted embedded activity
-        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .createActivityCount(1)
-                .setOrganizer(organizer)
-                .build();
-        final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord();
-        prepareActivityForAppTransition(activity);
-        doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity);
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // The animation will be animated remotely by client and all activities are input disabled
-        // for untrusted animation.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-        verify(activity).setDropInputForAnimation(true);
-        verify(activity).setDropInputMode(DropInputMode.ALL);
-
-        // Reset input after animation is finished.
-        clearInvocations(activity);
-        remoteAnimationRunner.finishAnimation();
-
-        verify(activity).setDropInputForAnimation(false);
-        verify(activity).setDropInputMode(DropInputMode.NONE);
-    }
-
-    /**
-     * We don't need to drop input for fully trusted embedding (system app, and embedding in the
-     * same app). This will allow users to do fast tapping.
-     */
-    @Test
-    public void testOverrideTaskFragmentAdapter_noInputProtectedForFullyTrustedAnimation() {
-        final Task task = createTask(mDisplayContent);
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
-
-        // Create a TaskFragment with only trusted embedded activity
-        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .createActivityCount(1)
-                .setOrganizer(organizer)
-                .build();
-        final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord();
-        prepareActivityForAppTransition(activity);
-        final int uid = mAtm.mTaskFragmentOrganizerController.getTaskFragmentOrganizerUid(
-                getITaskFragmentOrganizer(organizer));
-        doReturn(true).when(task).isFullyTrustedEmbedding(uid);
-        spyOn(mDisplayContent.mAppTransition);
-
-        // Prepare and start transition.
-        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
-
-        // The animation will be animated remotely by client, but input should not be dropped for
-        // fully trusted.
-        assertTrue(remoteAnimationRunner.isAnimationStarted());
-        verify(activity, never()).setDropInputForAnimation(true);
-        verify(activity, never()).setDropInputMode(DropInputMode.ALL);
-    }
-
-    @Test
-    public void testTransitionGoodToGoForTaskFragments() {
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final Task task = createTask(mDisplayContent);
-        final TaskFragment changeTaskFragment =
-                createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .setOrganizer(organizer)
-                .build();
-        prepareActivityForAppTransition(changeTaskFragment.getTopMostActivity());
-        spyOn(mDisplayContent.mAppTransition);
-        spyOn(emptyTaskFragment);
-
-        prepareAndTriggerAppTransition(
-                null /* openingActivity */, null /* closingActivity*/, changeTaskFragment);
-
-        // Transition not ready because there is an empty non-finishing TaskFragment.
-        verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any());
-
-        doReturn(true).when(emptyTaskFragment).hasChild();
-        emptyTaskFragment.remove(false /* withTransition */, "test");
-
-        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
-
-        // Transition ready because the empty (no running activity) TaskFragment is requested to be
-        // removed.
-        verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any());
-    }
-
-    @Test
-    public void testTransitionGoodToGoForTaskFragments_detachedApp() {
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer);
-        registerTaskFragmentOrganizer(iOrganizer);
-        final Task task = createTask(mDisplayContent);
-        final TaskFragment changeTaskFragment =
-                createTaskFragmentWithEmbeddedActivity(task, organizer);
-        final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .setOrganizer(organizer)
-                .build();
-        prepareActivityForAppTransition(changeTaskFragment.getTopMostActivity());
-        // To make sure that having a detached activity won't cause any issue.
-        final ActivityRecord detachedActivity = createActivityRecord(task);
-        detachedActivity.removeImmediately();
-        assertNull(detachedActivity.getRootTask());
-        spyOn(mDisplayContent.mAppTransition);
-        spyOn(emptyTaskFragment);
-
-        prepareAndTriggerAppTransition(
-                null /* openingActivity */, detachedActivity, changeTaskFragment);
-
-        // Transition not ready because there is an empty non-finishing TaskFragment.
-        verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any());
-
-        doReturn(true).when(emptyTaskFragment).hasChild();
-        emptyTaskFragment.remove(false /* withTransition */, "test");
-
-        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
-
-        // Transition ready because the empty (no running activity) TaskFragment is requested to be
-        // removed.
-        verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any());
-    }
-
-    /** Registers remote animation for the organizer. */
-    private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer,
-            TestRemoteAnimationRunner remoteAnimationRunner) {
-        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                remoteAnimationRunner, 10, 1);
-        final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer);
-        final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
-        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter);
-        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter);
-        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter);
-        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter);
-        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, adapter);
-        registerTaskFragmentOrganizer(iOrganizer);
-        mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition);
-    }
-
-    private static ITaskFragmentOrganizer getITaskFragmentOrganizer(
-            TaskFragmentOrganizer organizer) {
-        return ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
-    }
-
-    private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity,
-            @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) {
-        if (openingActivity != null) {
-            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
-            mDisplayContent.mOpeningApps.add(openingActivity);
-        }
-        if (closingActivity != null) {
-            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CLOSE, 0);
-            mDisplayContent.mClosingApps.add(closingActivity);
-        }
-        if (changingTaskFragment != null) {
-            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0);
-            mDisplayContent.mChangingContainers.add(changingTaskFragment);
-        }
-        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
-    }
-
-    private static void prepareActivityForAppTransition(ActivityRecord activity) {
-        // Transition will wait until all participated activities to be drawn.
-        activity.allDrawn = true;
-        // Skip manipulate the SurfaceControl.
-        doNothing().when(activity).setDropInputMode(anyInt());
-        // Assume the activity contains a window.
-        doReturn(true).when(activity).hasChild();
-        // Make sure activity can create remote animation target.
-        doReturn(mock(RemoteAnimationTarget.class)).when(activity).createRemoteAnimationTarget(
-                any());
-    }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
deleted file mode 100644
index 8553fbd..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ /dev/null
@@ -1,520 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_NONE;
-import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_UNSET;
-import static android.view.WindowManager.TRANSIT_OPEN;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
-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.verify;
-import static com.android.server.wm.WindowContainer.POSITION_TOP;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-
-import android.graphics.Rect;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-import android.util.ArraySet;
-import android.view.Display;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.WindowManager;
-import android.view.animation.Animation;
-import android.window.ITaskFragmentOrganizer;
-import android.window.TaskFragmentOrganizer;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.policy.TransitionAnimation;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test class for {@link AppTransition}.
- *
- * Build/Install/Run:
- *  atest WmTests:AppTransitionTests
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AppTransitionTests extends WindowTestsBase {
-    private DisplayContent mDc;
-
-    @Before
-    public void setUp() throws Exception {
-        doNothing().when(mWm.mRoot).performSurfacePlacement();
-        mDc = mWm.getDefaultDisplayContentLocked();
-    }
-
-    @Test
-    public void testKeyguardOverride() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE);
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY);
-        mDc.mOpeningApps.add(activity);
-        assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testKeyguardUnoccludeOcclude() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE);
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE);
-        mDc.mOpeningApps.add(activity);
-        assertEquals(TRANSIT_NONE,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-
-    }
-
-    @Test
-    public void testKeyguardKeep() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY);
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.mOpeningApps.add(activity);
-        assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testCrashing() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED);
-        mDc.mClosingApps.add(activity);
-        assertEquals(TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testKeepKeyguard_withCrashing() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY);
-        mDc.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED);
-        mDc.mClosingApps.add(activity);
-        assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testSkipTransitionAnimation() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        final ActivityRecord activity = createActivityRecord(dc);
-
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.prepareAppTransition(TRANSIT_CLOSE);
-        mDc.mClosingApps.add(activity);
-        assertEquals(TRANSIT_OLD_UNSET,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, true /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testTaskChangeWindowingMode() {
-        final ActivityRecord activity = createActivityRecord(mDc);
-
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.prepareAppTransition(TRANSIT_CHANGE);
-        mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority
-        mDc.mChangingContainers.add(activity.getTask());
-
-        assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testTaskFragmentChange() {
-        final ActivityRecord activity = createActivityRecord(mDc);
-        final TaskFragment taskFragment = new TaskFragment(mAtm, new Binder(),
-                true /* createdByOrganizer */, true /* isEmbedded */);
-        activity.getTask().addChild(taskFragment, POSITION_TOP);
-        activity.reparent(taskFragment, POSITION_TOP);
-
-        mDc.prepareAppTransition(TRANSIT_OPEN);
-        mDc.prepareAppTransition(TRANSIT_CHANGE);
-        mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority
-        mDc.mChangingContainers.add(taskFragment);
-
-        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CHANGE,
-                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
-    }
-
-    @Test
-    public void testTaskFragmentOpeningTransition() {
-        final ActivityRecord activity = createHierarchyForTaskFragmentTest();
-        activity.setVisible(false);
-
-        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
-        mDisplayContent.mOpeningApps.add(activity);
-        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_OPEN,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
-    }
-
-    @Test
-    public void testTaskFragmentClosingTransition() {
-        final ActivityRecord activity = createHierarchyForTaskFragmentTest();
-        activity.setVisible(true);
-
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
-        mDisplayContent.mClosingApps.add(activity);
-        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CLOSE,
-                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
-                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
-    }
-
-    /**
-     * Creates a {@link Task} with two {@link TaskFragment TaskFragments}.
-     * The bottom TaskFragment is to prevent
-     * {@link AppTransitionController#getAnimationTargets(ArraySet, ArraySet, boolean) the animation
-     * target} to promote to Task or above.
-     *
-     * @return The Activity to be put in either opening or closing Activity
-     */
-    private ActivityRecord createHierarchyForTaskFragmentTest() {
-        final Task parentTask = createTask(mDisplayContent);
-        final TaskFragment bottomTaskFragment = createTaskFragmentWithActivity(parentTask);
-        final ActivityRecord bottomActivity = bottomTaskFragment.getTopMostActivity();
-        bottomActivity.setOccludesParent(true);
-        bottomActivity.setVisible(true);
-
-        final TaskFragment verifiedTaskFragment = createTaskFragmentWithActivity(parentTask);
-        final ActivityRecord activity = verifiedTaskFragment.getTopMostActivity();
-        activity.setOccludesParent(true);
-
-        return activity;
-    }
-
-    @Test
-    public void testAppTransitionStateForMultiDisplay() {
-        // Create 2 displays & presume both display the state is ON for ready to display & animate.
-        final DisplayContent dc1 = createNewDisplay(Display.STATE_ON);
-        final DisplayContent dc2 = createNewDisplay(Display.STATE_ON);
-
-        // Create 2 app window tokens to represent 2 activity window.
-        final ActivityRecord activity1 = createActivityRecord(dc1);
-        final ActivityRecord activity2 = createActivityRecord(dc2);
-
-        activity1.allDrawn = true;
-        activity1.startingMoved = true;
-
-        // Simulate activity resume / finish flows to prepare app transition & set visibility,
-        // make sure transition is set as expected for each display.
-        dc1.prepareAppTransition(TRANSIT_OPEN);
-        dc2.prepareAppTransition(TRANSIT_CLOSE);
-        // One activity window is visible for resuming & the other activity window is invisible
-        // for finishing in different display.
-        activity1.setVisibility(true);
-        activity2.setVisibility(false);
-
-        // Make sure each display is in animating stage.
-        assertTrue(dc1.mOpeningApps.size() > 0);
-        assertTrue(dc2.mClosingApps.size() > 0);
-        assertTrue(dc1.isAppTransitioning());
-        assertTrue(dc2.isAppTransitioning());
-    }
-
-    @Test
-    public void testCleanAppTransitionWhenRootTaskReparent() {
-        // Create 2 displays & presume both display the state is ON for ready to display & animate.
-        final DisplayContent dc1 = createNewDisplay(Display.STATE_ON);
-        final DisplayContent dc2 = createNewDisplay(Display.STATE_ON);
-
-        final Task rootTask1 = createTask(dc1);
-        final Task task1 = createTaskInRootTask(rootTask1, 0 /* userId */);
-        final ActivityRecord activity1 = createNonAttachedActivityRecord(dc1);
-        task1.addChild(activity1, 0);
-
-        // Simulate same app is during opening / closing transition set stage.
-        dc1.mClosingApps.add(activity1);
-        assertTrue(dc1.mClosingApps.size() > 0);
-
-        dc1.prepareAppTransition(TRANSIT_OPEN);
-        assertTrue(dc1.mAppTransition.containsTransitRequest(TRANSIT_OPEN));
-        assertTrue(dc1.mAppTransition.isTransitionSet());
-
-        dc1.mOpeningApps.add(activity1);
-        assertTrue(dc1.mOpeningApps.size() > 0);
-
-        // Move root task to another display.
-        rootTask1.reparent(dc2.getDefaultTaskDisplayArea(), true);
-
-        // Verify if token are cleared from both pending transition list in former display.
-        assertFalse(dc1.mOpeningApps.contains(activity1));
-        assertFalse(dc1.mOpeningApps.contains(activity1));
-    }
-
-    @Test
-    public void testLoadAnimationSafely() {
-        DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        assertNull(dc.mAppTransition.loadAnimationSafely(
-                getInstrumentation().getTargetContext(), -1));
-    }
-
-    @Test
-    public void testCancelRemoteAnimationWhenFreeze() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        doReturn(false).when(dc).onDescendantOrientationChanged(any());
-        final WindowState exitingAppWindow = newWindowBuilder("exiting app",
-                TYPE_BASE_APPLICATION).setDisplay(dc).build();
-        final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord;
-        // Wait until everything in animation handler get executed to prevent the exiting window
-        // from being removed during WindowSurfacePlacer Traversal.
-        waitUntilHandlersIdle();
-
-        // Set a remote animator.
-        final TestRemoteAnimationRunner runner = new TestRemoteAnimationRunner();
-        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                runner, 100, 50, true /* changeNeedsSnapshot */);
-        // RemoteAnimationController will tracking RemoteAnimationAdapter's caller with calling pid.
-        adapter.setCallingPidUid(123, 456);
-
-        // Simulate activity finish flows to prepare app transition & set visibility,
-        // make sure transition is set as expected.
-        dc.prepareAppTransition(TRANSIT_CLOSE);
-        assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_CLOSE));
-        dc.mAppTransition.overridePendingAppTransitionRemote(adapter);
-        exitingActivity.setVisibility(false);
-        assertTrue(dc.mClosingApps.size() > 0);
-
-        // Make sure window is in animating stage before freeze, and cancel after freeze.
-        assertTrue(dc.isAppTransitioning());
-        assertFalse(runner.mCancelled);
-        dc.mAppTransition.freeze();
-        assertFalse(dc.isAppTransitioning());
-        assertTrue(runner.mCancelled);
-    }
-
-    @Test
-    public void testGetAnimationStyleResId() {
-        // Verify getAnimationStyleResId will return as LayoutParams.windowAnimations when without
-        // specifying window type.
-        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
-        attrs.windowAnimations = 0x12345678;
-        assertEquals(attrs.windowAnimations, mDc.mAppTransition.getAnimationStyleResId(attrs));
-
-        // Verify getAnimationStyleResId will return system resource Id when the window type is
-        // starting window.
-        attrs.type = TYPE_APPLICATION_STARTING;
-        assertEquals(mDc.mAppTransition.getDefaultWindowAnimationStyleResId(),
-                mDc.mAppTransition.getAnimationStyleResId(attrs));
-    }
-
-    @Test
-    public void testActivityRecordReparentedToTaskFragment() {
-        final ActivityRecord activity = createActivityRecord(mDc);
-        final SurfaceControl activityLeash = mock(SurfaceControl.class);
-        doNothing().when(activity).setDropInputMode(anyInt());
-        activity.setVisibility(true);
-        activity.setSurfaceControl(activityLeash);
-        final Task task = activity.getTask();
-
-        // Add a TaskFragment of half of the Task size.
-        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
-        final ITaskFragmentOrganizer iOrganizer =
-                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
-        registerTaskFragmentOrganizer(iOrganizer);
-        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .setOrganizer(organizer)
-                .build();
-        final Rect taskBounds = new Rect();
-        task.getBounds(taskBounds);
-        taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom);
-        spyOn(taskFragment);
-
-        assertTrue(mDc.mChangingContainers.isEmpty());
-        assertFalse(mDc.mAppTransition.isTransitionSet());
-
-        // Schedule app transition when reparent activity to a TaskFragment of different size.
-        final Rect startBounds = new Rect(activity.getBounds());
-        activity.reparent(taskFragment, POSITION_TOP);
-
-        // It should transit at TaskFragment level with snapshot on the activity surface.
-        verify(taskFragment).initializeChangeTransition(activity.getBounds(), activityLeash);
-        assertTrue(mDc.mChangingContainers.contains(taskFragment));
-        assertTrue(mDc.mAppTransition.containsTransitRequest(TRANSIT_CHANGE));
-    }
-
-    @Test
-    public void testGetNextAppTransitionBackgroundColor() {
-        assumeFalse(WindowManagerService.sEnableShellTransitions);
-
-        // No override by default.
-        assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
-
-        // Override with a custom color.
-        mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
-        final int testColor = 123;
-        mDc.mAppTransition.overridePendingAppTransition("testPackage", 0 /* enterAnim */,
-                0 /* exitAnim */, testColor, null /* startedCallback */, null /* endedCallback */,
-                false /* overrideTaskTransaction */);
-
-        assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
-        assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
-
-        // Override with ActivityEmbedding remote animation. Background color should be kept.
-        mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class),
-                false /* sync */, true /* isActivityEmbedding */);
-
-        assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
-        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
-
-        // Background color should not be cleared anymore after #clear().
-        mDc.mAppTransition.clear();
-        assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
-        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
-    }
-
-    @Test
-    public void testGetNextAppRequestedAnimation() {
-        assumeFalse(WindowManagerService.sEnableShellTransitions);
-        final String packageName = "testPackage";
-        final int enterAnimResId = 1;
-        final int exitAnimResId = 2;
-        final int testColor = 123;
-        final Animation enterAnim = mock(Animation.class);
-        final Animation exitAnim = mock(Animation.class);
-        final TransitionAnimation transitionAnimation = mDc.mAppTransition.mTransitionAnimation;
-        spyOn(transitionAnimation);
-        doReturn(enterAnim).when(transitionAnimation)
-                .loadAppTransitionAnimation(packageName, enterAnimResId);
-        doReturn(exitAnim).when(transitionAnimation)
-                .loadAppTransitionAnimation(packageName, exitAnimResId);
-
-        // No override by default.
-        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
-        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
-
-        // Override with a custom animation.
-        mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
-        mDc.mAppTransition.overridePendingAppTransition(packageName, enterAnimResId, exitAnimResId,
-                testColor, null /* startedCallback */, null /* endedCallback */,
-                false /* overrideTaskTransaction */);
-
-        assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
-        assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
-        assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
-
-        // Override with ActivityEmbedding remote animation. Custom animation should be kept.
-        mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class),
-                false /* sync */, true /* isActivityEmbedding */);
-
-        assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
-        assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
-        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
-
-        // Custom animation should not be cleared anymore after #clear().
-        mDc.mAppTransition.clear();
-        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
-        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
-    }
-
-    private class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
-        boolean mCancelled = false;
-        @Override
-        public void onAnimationStart(@WindowManager.TransitionOldType int transit,
-                RemoteAnimationTarget[] apps,
-                RemoteAnimationTarget[] wallpapers,
-                RemoteAnimationTarget[] nonApps,
-                IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
-        }
-
-        @Override
-        public void onAnimationCancelled() {
-            mCancelled = true;
-        }
-
-        @Override
-        public IBinder asBinder() {
-            return null;
-        }
-    }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index e0b700a..eaffc48 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -97,6 +97,7 @@
     public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse()
             throws Exception {
         doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+        doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
         doReturn(true).when(mMockResources).getBoolean(
                 eq(R.bool.config_isDesktopModeDevOptionSupported));
         disableEnforceDeviceRestriction();
@@ -148,6 +149,7 @@
     @Test
     public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() {
         doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+        doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
 
         assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue();
     }
@@ -176,21 +178,21 @@
 
     @Test
     public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() {
-        doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+        doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
 
-        assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
+        assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
     }
 
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @Test
     public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() {
-        assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
+        assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @Test
     public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() {
-        assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
+        assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
     }
 
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@@ -200,7 +202,7 @@
                 eq(R.bool.config_isDesktopModeDevOptionSupported)
         );
 
-        assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
+        assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
     }
 
     private void resetEnforceDeviceRestriction() throws Exception {
@@ -234,4 +236,4 @@
         Settings.Global.putInt(mContext.getContentResolver(),
                 DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting());
     }
-}
+}
\ No newline at end of file
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index fdde3b3..d305c2f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -1345,7 +1345,7 @@
     private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported,
             boolean enforceDeviceRestrictions) {
         doReturn(isDesktopModeSupported)
-                .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
+                .when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
         doReturn(enforceDeviceRestrictions)
                 .when(DesktopModeHelper::shouldEnforceDeviceRestrictions);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
index 285a5e2..ea21bb3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
@@ -23,6 +23,7 @@
 /** Robot for changing desktop windowing properties. */
 class DesktopWindowingRobot {
     void allowEnterDesktopMode(boolean isAllowed) {
-        doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
+        doReturn(isAllowed).when(() ->
+                DesktopModeHelper.canEnterDesktopMode(any()));
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 0964ebe..82435b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1172,11 +1172,12 @@
                 .setScreenOrientation(getRotatedOrientation(mDisplayContent)).build();
         prev.setVisibleRequested(false);
         final ActivityRecord top = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false)
                 .setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
         assertNotEquals(WindowConfiguration.ROTATION_UNDEFINED,
                 mDisplayContent.rotationForActivityInDifferentOrientation(top));
 
-        mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0);
+        requestTransition(top, WindowManager.TRANSIT_OPEN);
         top.setVisibility(true);
         mDisplayContent.updateOrientation();
         // The top uses "behind", so the orientation is decided by the previous.
@@ -1609,8 +1610,7 @@
         final ActivityRecord app = mAppWindow.mActivityRecord;
         app.setVisible(false);
         app.setVisibleRequested(false);
-        registerTestTransitionPlayer();
-        mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0);
+        requestTransition(app, WindowManager.TRANSIT_OPEN);
         app.setVisibility(true);
         final int newOrientation = getRotatedOrientation(mDisplayContent);
         app.setRequestedOrientation(newOrientation);
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 6c5fe1d..71e34ef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -53,6 +53,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 
 import androidx.test.filters.SmallTest;
 
@@ -400,9 +401,9 @@
         assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars()));
         assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars()));
 
-        mAppWindow.setRequestedVisibleTypes(
+        final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes(
                 navigationBars() | statusBars(), navigationBars() | statusBars());
-        policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */);
+        policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */);
         waitUntilWindowAnimatorIdle();
 
         controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 973c8d0..5525bae 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -52,6 +52,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.WindowInsets.Type.InsetsType;
 
 import androidx.test.filters.SmallTest;
 
@@ -201,8 +202,8 @@
         getController().getOrCreateSourceProvider(ID_IME, ime())
                 .setWindowContainer(mImeWindow, null, null);
         getController().onImeControlTargetChanged(base);
-        base.setRequestedVisibleTypes(ime(), ime());
-        getController().onRequestedVisibleTypesChanged(base, null /* statsToken */);
+        final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime());
+        getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */);
         if (android.view.inputmethod.Flags.refactorInsetsController()) {
             // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called.
             mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN;
@@ -509,8 +510,8 @@
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.updateImeInputAndControlTarget(app);
 
-        app.setRequestedVisibleTypes(ime(), ime());
-        getController().onRequestedVisibleTypesChanged(app, null /* statsToken */);
+        final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime());
+        getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */);
         assertTrue(ime.getControllableInsetProvider().getSource().isVisible());
 
         if (android.view.inputmethod.Flags.refactorInsetsController()) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
new file mode 100644
index 0000000..db90c28
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Display.FLAG_PRESENTATION;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.graphics.Rect;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+import android.view.IWindow;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest WmTests:PresentationControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class PresentationControllerTests extends WindowTestsBase {
+
+    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+    @Test
+    public void testPresentationHidesActivitiesBehind() {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.copyFrom(mDisplayInfo);
+        displayInfo.flags = FLAG_PRESENTATION;
+        final DisplayContent dc = createNewDisplay(displayInfo);
+        final int displayId = dc.getDisplayId();
+        doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
+        final ActivityRecord activity = createActivityRecord(createTask(dc));
+        assertTrue(activity.isVisible());
+
+        doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+        final int uid = 100000; // uid for non-system user
+        final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+        final int userId = UserHandle.getUserId(uid);
+        doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+        final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_PRESENTATION);
+
+        final IWindow clientWindow = new TestIWindow();
+        final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId,
+                userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+                new InsetsSourceControl.Array(), new Rect(), new float[1]);
+        assertTrue(result >= WindowManagerGlobal.ADD_OKAY);
+        assertFalse(activity.isVisible());
+
+        final WindowState window = mWm.windowForClientLocked(session, clientWindow, false);
+        window.removeImmediately();
+        assertTrue(activity.isVisible());
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index fc4f54a..e4a1bf6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -454,31 +454,6 @@
     }
 
     @Test
-    public void testMovingBottomMostRootTaskActivityToPinnedRootTask() {
-        final Task fullscreenTask = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask(
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        final ActivityRecord firstActivity = new ActivityBuilder(mAtm)
-                .setTask(fullscreenTask).build();
-        final Task task = firstActivity.getTask();
-
-        final ActivityRecord secondActivity = new ActivityBuilder(mAtm)
-                .setTask(fullscreenTask).build();
-
-        fullscreenTask.moveTaskToBack(task);
-
-        // Ensure full screen task has both tasks.
-        ensureTaskPlacement(fullscreenTask, firstActivity, secondActivity);
-        assertEquals(task.getTopMostActivity(), secondActivity);
-        firstActivity.setState(STOPPED, "testMovingBottomMostRootTaskActivityToPinnedRootTask");
-
-
-        // Move first activity to pinned root task.
-        mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, "initialMove");
-
-        assertTrue(firstActivity.mRequestForceTransition);
-    }
-
-    @Test
     public void testMultipleActivitiesTaskEnterPip() {
         // Enable shell transition because the order of setting windowing mode is different.
         registerTestTransitionPlayer();
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 dba463a..95bca2b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1811,9 +1811,9 @@
         }
         addStatusBar(mActivity.mDisplayContent);
 
-        mActivity.setVisible(false);
-        mActivity.mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_OPEN);
-        mActivity.mDisplayContent.mOpeningApps.add(mActivity);
+        mActivity.setVisibleRequested(false);
+        requestTransition(mActivity, WindowManager.TRANSIT_OPEN);
+        mActivity.setVisibility(true);
         final float maxAspect = 1.8f;
         prepareUnresizable(mActivity, maxAspect, SCREEN_ORIENTATION_LANDSCAPE);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index 7dba142..2544550 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -22,6 +22,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -69,7 +70,6 @@
 
     private MyAnimatable mAnimatable;
     private MyAnimatable mAnimatable2;
-    private DeferFinishAnimatable mDeferFinishAnimatable;
 
     @Before
     public void setUp() throws Exception {
@@ -77,14 +77,12 @@
 
         mAnimatable = new MyAnimatable(mWm, mTransaction);
         mAnimatable2 = new MyAnimatable(mWm, mTransaction);
-        mDeferFinishAnimatable = new DeferFinishAnimatable(mWm, mTransaction);
     }
 
     @After
     public void tearDown() {
         mAnimatable = null;
         mAnimatable2 = null;
-        mDeferFinishAnimatable = null;
     }
 
     @Test
@@ -202,41 +200,33 @@
     }
 
     @Test
-    public void testDeferFinish() {
-
-        // Start animation
-        final OnAnimationFinishedCallback onFinishedCallback = startDeferFinishAnimatable(mSpec);
-
-        // Finish the animation but then make sure we are deferring.
-        onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec);
-        assertAnimating(mDeferFinishAnimatable);
-
-        // Now end defer finishing.
-        mDeferFinishAnimatable.mEndDeferFinishCallback.run();
-        assertNotAnimating(mAnimatable2);
-        assertTrue(mDeferFinishAnimatable.mFinishedCallbackCalled);
-        assertEquals(ANIMATION_TYPE_APP_TRANSITION, mDeferFinishAnimatable.mFinishedAnimationType);
-        verify(mTransaction).remove(eq(mDeferFinishAnimatable.mLeash));
-    }
-
-    @Test
     public void testDeferFinishDoNotFinishNextAnimation() {
+        final DeferredFinishAdapter deferredFinishAdapter = new DeferredFinishAdapter();
+        spyOn(deferredFinishAdapter);
         // Start the first animation.
-        final OnAnimationFinishedCallback onFinishedCallback = startDeferFinishAnimatable(mSpec);
-        onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec);
+        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, deferredFinishAdapter,
+                true /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION);
+        assertAnimating(mAnimatable);
+        final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+                OnAnimationFinishedCallback.class);
+        verify(deferredFinishAdapter).startAnimation(any(), any(),
+                eq(ANIMATION_TYPE_WINDOW_ANIMATION), callbackCaptor.capture());
+        final OnAnimationFinishedCallback onFinishedCallback = callbackCaptor.getValue();
+        onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION,
+                deferredFinishAdapter);
         // The callback is the resetAndInvokeFinish in {@link SurfaceAnimator#getFinishedCallback}.
-        final Runnable firstDeferFinishCallback = mDeferFinishAnimatable.mEndDeferFinishCallback;
+        final Runnable firstDeferFinishCallback = deferredFinishAdapter.mEndDeferFinishCallback;
 
         // Start the second animation.
-        mDeferFinishAnimatable.mSurfaceAnimator.cancelAnimation();
-        startDeferFinishAnimatable(mSpec2);
-        mDeferFinishAnimatable.mFinishedCallbackCalled = false;
+        mAnimatable.mSurfaceAnimator.cancelAnimation();
+        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec2,
+                true /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION);
+        mAnimatable.mFinishedCallbackCalled = false;
 
-        // Simulate the first deferred callback is executed from
-        // {@link AnimatingActivityRegistry#endDeferringFinished}.
+        // Simulate the first deferred callback is executed.
         firstDeferFinishCallback.run();
         // The second animation should not be finished.
-        assertFalse(mDeferFinishAnimatable.mFinishedCallbackCalled);
+        assertFalse(mAnimatable.mFinishedCallbackCalled);
     }
 
     @Test
@@ -260,17 +250,6 @@
         verify(mTransaction).remove(eq(deferredFinishAdapter.mAnimationLeash));
     }
 
-    private OnAnimationFinishedCallback startDeferFinishAnimatable(AnimationAdapter anim) {
-        mDeferFinishAnimatable.mSurfaceAnimator.startAnimation(mTransaction, anim,
-                true /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
-        final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
-                OnAnimationFinishedCallback.class);
-        assertAnimating(mDeferFinishAnimatable);
-        verify(anim).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION),
-                callbackCaptor.capture());
-        return callbackCaptor.getValue();
-    }
-
     private void assertAnimating(MyAnimatable animatable) {
         assertTrue(animatable.mSurfaceAnimator.isAnimating());
         assertNotNull(animatable.mSurfaceAnimator.getAnimation());
@@ -370,21 +349,6 @@
         };
     }
 
-    private static class DeferFinishAnimatable extends MyAnimatable {
-
-        Runnable mEndDeferFinishCallback;
-
-        DeferFinishAnimatable(WindowManagerService wm, Transaction transaction) {
-            super(wm, transaction);
-        }
-
-        @Override
-        public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
-            mEndDeferFinishCallback = endDeferFinishCallback;
-            return true;
-        }
-    }
-
     private static class DeferredFinishAdapter implements AnimationAdapter {
 
         private Runnable mEndDeferFinishCallback;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 0014465..edffab8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -32,8 +32,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
-import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -85,12 +83,8 @@
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.util.ArraySet;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.WindowInsets;
@@ -1055,25 +1049,6 @@
     }
 
     @Test
-    public void testTaskCanApplyAnimation() {
-        final Task rootTask = createTask(mDisplayContent);
-        final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent, task);
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent, task);
-        verifyWindowContainerApplyAnimation(task, activity1, activity2);
-    }
-
-    @Test
-    public void testRootTaskCanApplyAnimation() {
-        final Task rootTask = createTask(mDisplayContent);
-        final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
-                createTaskInRootTask(rootTask, 0 /* userId */));
-        final ActivityRecord activity1 = createActivityRecord(mDisplayContent,
-                createTaskInRootTask(rootTask, 0 /* userId */));
-        verifyWindowContainerApplyAnimation(rootTask, activity1, activity2);
-    }
-
-    @Test
     public void testGetDisplayArea() {
         // WindowContainer
         final WindowContainer windowContainer = new WindowContainer(mWm);
@@ -1103,59 +1078,6 @@
         assertEquals(displayArea, displayArea.getDisplayArea());
     }
 
-    private void verifyWindowContainerApplyAnimation(WindowContainer wc, ActivityRecord act,
-            ActivityRecord act2) {
-        // Initial remote animation for app transition.
-        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                new IRemoteAnimationRunner.Stub() {
-                    @Override
-                    public void onAnimationStart(@WindowManager.TransitionOldType int transit,
-                            RemoteAnimationTarget[] apps,
-                            RemoteAnimationTarget[] wallpapers,
-                            RemoteAnimationTarget[] nonApps,
-                            IRemoteAnimationFinishedCallback finishedCallback) {
-                        try {
-                            finishedCallback.onAnimationFinished();
-                        } catch (RemoteException e) {
-                            e.printStackTrace();
-                        }
-                    }
-
-                    @Override
-                    public void onAnimationCancelled() {
-                    }
-                }, 0, 0, false);
-        adapter.setCallingPidUid(123, 456);
-        wc.getDisplayContent().prepareAppTransition(TRANSIT_OPEN);
-        wc.getDisplayContent().mAppTransition.overridePendingAppTransitionRemote(adapter);
-        spyOn(wc);
-        doReturn(true).when(wc).okToAnimate();
-
-        // Make sure animating state is as expected after applied animation.
-
-        // Animation target is promoted from act to wc. act2 is a descendant of wc, but not a source
-        // of the animation.
-        ArrayList<WindowContainer<WindowState>> sources = new ArrayList<>();
-        sources.add(act);
-        assertTrue(wc.applyAnimation(null, TRANSIT_OLD_TASK_OPEN, true, false, sources));
-
-        assertEquals(act, wc.getTopMostActivity());
-        assertTrue(wc.isAnimating());
-        assertTrue(wc.isAnimating(0, ANIMATION_TYPE_APP_TRANSITION));
-        assertTrue(wc.getAnimationSources().contains(act));
-        assertFalse(wc.getAnimationSources().contains(act2));
-        assertTrue(act.isAnimating(PARENTS));
-        assertTrue(act.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION));
-        assertEquals(wc, act.getAnimatingContainer(PARENTS, ANIMATION_TYPE_APP_TRANSITION));
-
-        // Make sure animation finish callback will be received and reset animating state after
-        // animation finish.
-        wc.getDisplayContent().mAppTransition.goodToGo(TRANSIT_OLD_TASK_OPEN, act);
-        verify(wc).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), any());
-        assertFalse(wc.isAnimating());
-        assertFalse(act.isAnimating(PARENTS));
-    }
-
     @Test
     public void testRegisterWindowContainerListener() {
         final WindowContainer container = new WindowContainer(mWm);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 1323d8a..71e84c0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -26,7 +26,6 @@
 import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.FLAG_OWN_FOCUS;
-import static android.view.Display.FLAG_PRESENTATION;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
@@ -55,7 +54,6 @@
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -102,7 +100,6 @@
 import android.util.ArraySet;
 import android.util.MergedConfiguration;
 import android.view.ContentRecordingSession;
-import android.view.DisplayInfo;
 import android.view.IWindow;
 import android.view.InputChannel;
 import android.view.InputDevice;
@@ -1409,38 +1406,6 @@
         assertEquals(activityWindowInfo2, activityWindowInfo3);
     }
 
-    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
-    @Test
-    public void testPresentationHidesActivitiesBehind() {
-        DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.copyFrom(mDisplayInfo);
-        displayInfo.flags = FLAG_PRESENTATION;
-        DisplayContent dc = createNewDisplay(displayInfo);
-        int displayId = dc.getDisplayId();
-        doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
-        ActivityRecord activity = createActivityRecord(createTask(dc));
-        assertTrue(activity.isVisible());
-
-        doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
-        int uid = 100000; // uid for non-system user
-        Session session = createTestSession(mAtm, 1234 /* pid */, uid);
-        int userId = UserHandle.getUserId(uid);
-        doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
-        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
-                LayoutParams.TYPE_PRESENTATION);
-
-        final IWindow clientWindow = new TestIWindow();
-        int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId,
-                userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
-                new InsetsSourceControl.Array(), new Rect(), new float[1]);
-        assertTrue(result >= WindowManagerGlobal.ADD_OKAY);
-        assertFalse(activity.isVisible());
-
-        final WindowState window = mWm.windowForClientLocked(session, clientWindow, false);
-        window.removeImmediately();
-        assertTrue(activity.isVisible());
-    }
-
     @Test
     public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() {
         doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 1281be51..7030d986 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -26,6 +26,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -33,6 +34,8 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
 import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+import static android.content.res.Configuration.UI_MODE_NIGHT_NO;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
@@ -1983,6 +1986,30 @@
         testSetAlwaysOnTop(displayArea);
     }
 
+    @Test
+    public void testConfigurationsAreEqualForOrganizer() {
+        Configuration config1 = new Configuration();
+        config1.smallestScreenWidthDp = 300;
+        config1.uiMode = UI_MODE_NIGHT_YES;
+
+        Configuration config2 = new Configuration(config1);
+        config2.uiMode = UI_MODE_NIGHT_NO;
+
+        Configuration config3 = new Configuration(config1);
+        config3.smallestScreenWidthDp = 500;
+
+        // Should be equal for non-controllable configuration changes.
+        assertTrue(WindowOrganizerController.configurationsAreEqualForOrganizer(config1, config2));
+
+        // Should be unequal for non-controllable configuration changes if the organizer is
+        // interested in that change.
+        assertFalse(WindowOrganizerController.configurationsAreEqualForOrganizer(
+                config1, config2, CONFIG_UI_MODE));
+
+        // Should be unequal for controllable configuration changes.
+        assertFalse(WindowOrganizerController.configurationsAreEqualForOrganizer(config1, config3));
+    }
+
     private void testSetAlwaysOnTop(WindowContainer wc) {
         final WindowContainerTransaction t = new WindowContainerTransaction();
         t.setAlwaysOnTop(wc.mRemoteToken.toWindowContainerToken(), true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index cff172f..a718c06 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1282,7 +1282,6 @@
         // Simulate app plays closing transition to app2.
         app.mActivityRecord.commitVisibility(false, false);
         assertTrue(app.mActivityRecord.mLastImeShown);
-        assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
         // Verify the IME insets is visible on app, but not for app2 during app task switching.
         assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
@@ -1305,7 +1304,7 @@
 
         // Simulate app2 in multi-window mode is going to background to switch to the fullscreen
         // app which requests IME with updating all windows Insets State when IME is above app.
-        app2.mActivityRecord.mImeInsetsFrozenUntilStartInput = true;
+        app2.mActivityRecord.setVisibleRequested(false);
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
         app.setRequestedVisibleTypes(ime(), ime());
@@ -1324,7 +1323,6 @@
         mDisplayContent.setImeLayeringTarget(app2);
         app.mActivityRecord.commitVisibility(false, false);
         assertTrue(app.mActivityRecord.mLastImeShown);
-        assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
         // Verify the IME insets is still visible on app, but not for app2 during task switching.
         assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index b16f528..7f9e591 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -961,6 +961,15 @@
         return testPlayer;
     }
 
+    void requestTransition(WindowContainer<?> wc, int transit) {
+        final TransitionController controller = mRootWindowContainer.mTransitionController;
+        if (controller.getTransitionPlayer() == null) {
+            registerTestTransitionPlayer();
+        }
+        controller.requestTransitionIfNeeded(transit, 0 /* flags */, null /* trigger */,
+                wc.mDisplayContent);
+    }
+
     /** Overrides the behavior of config_reverseDefaultRotation for the given display. */
     void setReverseDefaultRotation(DisplayContent dc, boolean reverse) {
         final DisplayRotation displayRotation = dc.getDisplayRotation();
@@ -1417,7 +1426,9 @@
             activity.setProcess(wpc);
 
             // Resume top activities to make sure all other signals in the system are connected.
-            mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
+            if (mVisible) {
+                mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
+            }
             return activity;
         }
     }
diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java
index 55a8923..86468b0 100644
--- a/services/usb/java/com/android/server/usb/UsbPortManager.java
+++ b/services/usb/java/com/android/server/usb/UsbPortManager.java
@@ -200,7 +200,11 @@
         mHandler.sendEmptyMessage(MSG_SYSTEM_READY);
     }
 
-    private void updateContaminantNotification() {
+    private void updateContaminantNotificationLocked() {
+        if (mNotificationManager == null) {
+            return;
+        }
+
         PortInfo currentPortInfo = null;
         Resources r = mContext.getResources();
         int contaminantStatus = UsbPortStatus.CONTAMINANT_DETECTION_NOT_DETECTED;
@@ -1171,7 +1175,7 @@
     private void handlePortLocked(PortInfo portInfo, IndentingPrintWriter pw) {
         sendPortChangedBroadcastLocked(portInfo);
         logToStatsd(portInfo, pw);
-        updateContaminantNotification();
+        updateContaminantNotificationLocked();
     }
 
     private void handlePortAddedLocked(PortInfo portInfo, IndentingPrintWriter pw) {
@@ -1433,6 +1437,9 @@
                 case MSG_SYSTEM_READY: {
                     mNotificationManager = (NotificationManager)
                             mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+                    synchronized (mLock) {
+                        updateContaminantNotificationLocked();
+                    }
                     break;
                 }
             }
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index e0af223..d2741ac 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4821,10 +4821,14 @@
                     + "Invalid subscriptionId: " + subscriptionId);
         }
 
+        String contextPkg = mContext != null ? mContext.getOpPackageName() : "<unknown>";
+        String contextAttributionTag = mContext != null ? mContext.getAttributionTag() : null;
+
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId);
+                return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId, contextPkg,
+                        contextAttributionTag);
             } else {
                 throw new IllegalStateException("subscription service unavailable.");
             }
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 1bfec29..a974c61 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -347,13 +347,17 @@
      * Returns whether the given subscription is associated with the calling user.
      *
      * @param subscriptionId the subscription ID of the subscription
+     * @param callingPackage The package maing the call
+     * @param callingFeatureId The feature in the package
+
      * @return {@code true} if the subscription is associated with the user that the current process
      *         is running in; {@code false} otherwise.
      *
      * @throws IllegalArgumentException if subscription doesn't exist.
      * @throws SecurityException if the caller doesn't have permissions required.
      */
-    boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId);
+    boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId, String callingPackage,
+            String callingFeatureId);
 
     /**
      * Check if subscription and user are associated with each other.
diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml
index 37321ad8..758852b 100644
--- a/tests/AttestationVerificationTest/AndroidManifest.xml
+++ b/tests/AttestationVerificationTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="android.security.attestationverification">
 
-    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" />
     <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" />
 
     <application>
diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json
new file mode 100644
index 0000000..2a3ba5e
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json
@@ -0,0 +1,12 @@
+{
+  "entries": {
+    "6681152659205225093" : {
+      "status": "REVOKED",
+      "reason": "KEY_COMPROMISE"
+    },
+    "8350192447815228107" : {
+      "status": "REVOKED",
+      "reason": "KEY_COMPROMISE"
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json
new file mode 100644
index 0000000..e22a834
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json
@@ -0,0 +1,16 @@
+{
+  "entries": {
+    "6681152659205225093" : {
+      "status": "REVOKED",
+      "reason": "KEY_COMPROMISE"
+    },
+    "353017e73dc205a73a9c3de142230370" : {
+      "status": "REVOKED",
+      "reason": "KEY_COMPROMISE"
+    },
+    "8350192447815228107" : {
+      "status": "REVOKED",
+      "reason": "KEY_COMPROMISE"
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
new file mode 100644
index 0000000..c38517a
--- /dev/null
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CertificateRevocationStatusManagerTest {
+
+    private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem";
+    private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem";
+    private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json";
+    private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST =
+            "test_revocation_list_no_test_certs.json";
+    private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST =
+            "test_revocation_list_with_test_certs.json";
+    private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt";
+    private static final String FILE_URL_PREFIX = "file://";
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+    private CertificateFactory mFactory;
+    private List<X509Certificate> mCertificates1;
+    private List<X509Certificate> mCertificates2;
+    private File mRevocationListFile;
+    private String mRevocationListUrl;
+    private String mNonExistentRevocationListUrl;
+    private File mRevocationStatusFile;
+    private CertificateRevocationStatusManager mCertificateRevocationStatusManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mFactory = CertificateFactory.getInstance("X.509");
+        mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1);
+        mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2);
+        mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME);
+        mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath();
+        File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist");
+        mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath();
+        mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mRevocationListFile.delete();
+        mRevocationStatusFile.delete();
+    }
+
+    @Test
+    public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+    }
+
+    @Test
+    public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
+    @Test
+    public void
+            checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException()
+                    throws Exception {
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
+    @Test
+    public void checkRevocationStatus_savesRevocationStatus() throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+
+        assertThat(mRevocationStatusFile.length()).isGreaterThan(0);
+    }
+
+    @Test
+    public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException()
+            throws Exception {
+        // call checkRevocationStatus once to save the revocation status
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+        // call checkRevocationStatus again with mNonExistentRevocationListUrl
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+    }
+
+    @Test
+    public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception()
+            throws Exception {
+        // call checkRevocationStatus once to save the revocation status for mCertificates2
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2);
+        // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for
+        // mCertificates1
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
+    @Test
+    public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception()
+            throws Exception {
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime expiredStatusDate =
+                now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1);
+        Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
+        lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate);
+        for (int i = 1; i < mCertificates1.size(); i++) {
+            lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now);
+        }
+        mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
+
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
+    @Test
+    public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException()
+            throws Exception {
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+        LocalDateTime bearlyNotExpiredStatusDate =
+                LocalDateTime.now()
+                        .minusDays(
+                                CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1);
+        Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
+        for (X509Certificate certificate : mCertificates1) {
+            lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate);
+        }
+        mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
+
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+    }
+
+    @Test
+    public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception {
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+        Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+        for (X509Certificate certificate : mCertificates1) {
+            areCertificatesRevoked.put(getSerialNumber(certificate), false);
+        }
+
+        mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
+
+        // no exception
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+        // revoke one certificate and try again
+        areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true);
+        mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
+    @Test
+    public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        // populate the revocation status file
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+        // Sleep for 2 second so that the current time changes
+        SystemClock.sleep(2000);
+        LocalDateTime timestampBeforeUpdate = LocalDateTime.now();
+        JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList();
+        List<String> otherCertificatesToCheck = new ArrayList<>();
+        String serialNumber1 = "1234567"; // not revoked
+        String serialNumber2 = "8350192447815228107"; // revoked
+        String serialNumber3 = "987654"; // not revoked
+        otherCertificatesToCheck.add(serialNumber1);
+        otherCertificatesToCheck.add(serialNumber2);
+        otherCertificatesToCheck.add(serialNumber3);
+
+        mCertificateRevocationStatusManager
+                .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+                        revocationList, otherCertificatesToCheck);
+
+        Map<String, LocalDateTime> lastRevocationCheckData =
+                mCertificateRevocationStatusManager.getLastRevocationCheckData();
+        assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate);
+        assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked
+        assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate);
+        // validate that the existing certificates on the file got updated too
+        for (X509Certificate certificate : mCertificates1) {
+            assertThat(lastRevocationCheckData.get(getSerialNumber(certificate)))
+                    .isAtLeast(timestampBeforeUpdate);
+        }
+    }
+
+    private List<X509Certificate> getCertificateChain(String fileName) throws Exception {
+        Collection<? extends Certificate> certificates =
+                mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName));
+        ArrayList<X509Certificate> x509Certs = new ArrayList<>();
+        for (Certificate cert : certificates) {
+            x509Certs.add((X509Certificate) cert);
+        }
+        return x509Certs;
+    }
+
+    private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception {
+        byte[] data;
+        try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) {
+            data = in.readAllBytes();
+        }
+        try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) {
+            fileOutputStream.write(data);
+        }
+    }
+
+    private String getSerialNumber(X509Certificate certificate) {
+        return certificate.getSerialNumber().toString(16);
+    }
+}
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index 12670cd..ac704e5 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -52,10 +52,12 @@
         <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
         <option name="run-command" value="settings put system show_touches 1"/>
         <option name="run-command" value="settings put system pointer_location 1"/>
+        <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
         <option name="teardown-command"
                 value="settings delete secure show_ime_with_hard_keyboard"/>
         <option name="teardown-command" value="settings delete system show_touches"/>
         <option name="teardown-command" value="settings delete system pointer_location"/>
+        <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
         <option name="teardown-command"
                 value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
     </target_preparer>
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index 481a8bb..1b2007d 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -50,10 +50,12 @@
         <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
         <option name="run-command" value="settings put system show_touches 1"/>
         <option name="run-command" value="settings put system pointer_location 1"/>
+        <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
         <option name="teardown-command"
                 value="settings delete secure show_ime_with_hard_keyboard"/>
         <option name="teardown-command" value="settings delete system show_touches"/>
         <option name="teardown-command" value="settings delete system pointer_location"/>
+        <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
         <option name="teardown-command"
                 value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
     </target_preparer>
diff --git a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt
index ae32bda..bcff2fc 100644
--- a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt
+++ b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt
@@ -16,17 +16,11 @@
 
 package android.hardware.input
 
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
 import android.content.ContextWrapper
 import android.graphics.drawable.Drawable
 import android.platform.test.annotations.Presubmit
-import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.hardware.input.Flags
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.junit.MockitoJUnitRunner
@@ -46,9 +40,6 @@
         const val HEIGHT = 100
     }
 
-    @get:Rule
-    val setFlagsRule = SetFlagsRule()
-
     private fun createDrawable(): Drawable? {
         val context = ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())
         val inputManager = context.getSystemService(InputManager::class.java)!!
@@ -56,16 +47,9 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG)
     fun testKeyboardLayoutDrawable_hasCorrectDimensions() {
         val drawable = createDrawable()!!
         assertEquals(WIDTH, drawable.intrinsicWidth)
         assertEquals(HEIGHT, drawable.intrinsicHeight)
     }
-
-    @Test
-    @DisableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG)
-    fun testKeyboardLayoutDrawable_isNull_ifFlagOff() {
-        assertNull(createDrawable())
-    }
 }
\ No newline at end of file
diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
index c2f9adf..cc58bbc 100644
--- a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
+++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
@@ -21,9 +21,7 @@
 import android.os.Handler
 import android.os.HandlerExecutor
 import android.os.test.TestLooper
-import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.Presubmit
-import android.platform.test.flag.junit.SetFlagsRule
 import android.view.KeyEvent
 import androidx.test.core.app.ApplicationProvider
 import com.android.server.testutils.any
@@ -50,12 +48,9 @@
  */
 @Presubmit
 @RunWith(MockitoJUnitRunner::class)
-@EnableFlags(com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG)
 class StickyModifierStateListenerTest {
 
     @get:Rule
-    val rule = SetFlagsRule()
-    @get:Rule
     val inputManagerRule = MockInputManagerRule()
 
     private val testLooper = TestLooper()
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 5259455..b22e42d 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -559,9 +559,6 @@
 
     @Test
     fun handleKeyGestures_a11yBounceKeysShortcut() {
-        ExtendedMockito.doReturn(true).`when` {
-            InputSettings.isAccessibilityBounceKeysFeatureEnabled()
-        }
         val toggleBounceKeysEvent =
             KeyGestureEvent.Builder()
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS)
@@ -594,9 +591,6 @@
 
     @Test
     fun handleKeyGestures_a11yStickyKeysShortcut() {
-        ExtendedMockito.doReturn(true).`when` {
-            InputSettings.isAccessibilityStickyKeysFeatureEnabled()
-        }
         val toggleStickyKeysEvent =
             KeyGestureEvent.Builder()
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS)
@@ -610,9 +604,6 @@
 
     @Test
     fun handleKeyGestures_a11ySlowKeysShortcut() {
-        ExtendedMockito.doReturn(true).`when` {
-            InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()
-        }
         val toggleSlowKeysEvent =
             KeyGestureEvent.Builder()
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS)
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 37bdf6b..88e8496 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -754,9 +754,6 @@
     @EnableFlags(
         com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SHORTCUT_CONTROL,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
         com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
         com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES,
@@ -773,9 +770,6 @@
     @EnableFlags(
         com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SHORTCUT_CONTROL,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG,
-        com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
         com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
         com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES,
@@ -1438,6 +1432,58 @@
         )
     }
 
+    @Test
+    @Parameters(method = "customInputGesturesTestArguments")
+    fun testCustomKeyGestureRestoredFromBackup(test: TestData) {
+        val userId = 10
+        setupKeyGestureController()
+        val builder = InputGestureData.Builder()
+            .setKeyGestureType(test.expectedKeyGestureType)
+            .setTrigger(
+                InputGestureData.createKeyTrigger(
+                    test.expectedKeys[0],
+                    test.expectedModifierState
+                )
+            )
+        if (test.expectedAppLaunchData != null) {
+            builder.setAppLaunchData(test.expectedAppLaunchData)
+        }
+        val inputGestureData = builder.build()
+
+        keyGestureController.setCurrentUserId(userId)
+        testLooper.dispatchAll()
+        keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+        testLooper.dispatchAll()
+        val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+        // Delete the old data and reinitialize the controller simulating a "fresh" install.
+        tempFile.delete()
+        setupKeyGestureController()
+        keyGestureController.setCurrentUserId(userId)
+        testLooper.dispatchAll()
+
+        // Initially there should be no gestures registered.
+        var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+        assertEquals(
+            "Test: $test doesn't produce correct number of saved input gestures",
+            0,
+            savedInputGestures.size
+        )
+
+        // After the restore, there should be the original gesture re-registered.
+        keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+        savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+        assertEquals(
+            "Test: $test doesn't produce correct number of saved input gestures",
+            1,
+            savedInputGestures.size
+        )
+        assertEquals(
+            "Test: $test doesn't produce correct input gesture data", inputGestureData,
+            InputGestureData(savedInputGestures[0])
+        )
+    }
+
     class TouchpadTestData(
         val name: String,
         val touchpadGestureType: Int,
@@ -1549,6 +1595,53 @@
         )
     }
 
+
+    @Test
+    @Parameters(method = "customTouchpadGesturesTestArguments")
+    fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) {
+        val userId = 10
+        setupKeyGestureController()
+        val builder = InputGestureData.Builder()
+            .setKeyGestureType(test.expectedKeyGestureType)
+            .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType))
+        if (test.expectedAppLaunchData != null) {
+            builder.setAppLaunchData(test.expectedAppLaunchData)
+        }
+        val inputGestureData = builder.build()
+        keyGestureController.setCurrentUserId(userId)
+        testLooper.dispatchAll()
+        keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+        testLooper.dispatchAll()
+        val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+        // Delete the old data and reinitialize the controller simulating a "fresh" install.
+        tempFile.delete()
+        setupKeyGestureController()
+        keyGestureController.setCurrentUserId(userId)
+        testLooper.dispatchAll()
+
+        // Initially there should be no gestures registered.
+        var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+        assertEquals(
+            "Test: $test doesn't produce correct number of saved input gestures",
+            0,
+            savedInputGestures.size
+        )
+
+        // After the restore, there should be the original gesture re-registered.
+        keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+        savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+        assertEquals(
+            "Test: $test doesn't produce correct number of saved input gestures",
+            1,
+            savedInputGestures.size
+        )
+        assertEquals(
+            "Test: $test doesn't produce correct input gesture data", inputGestureData,
+            InputGestureData(savedInputGestures[0])
+        )
+    }
+
     private fun testKeyGestureInternal(test: TestData) {
         val handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt
index e855786..e1294b1 100644
--- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt
+++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt
@@ -19,12 +19,9 @@
 import android.content.Context
 import android.hardware.input.KeyboardLayout
 import android.os.LocaleList
-import android.platform.test.flag.junit.SetFlagsRule
-import com.android.hardware.input.Flags
 import java.util.Locale
 import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import platform.test.screenshot.DeviceEmulationSpec
@@ -38,18 +35,14 @@
         fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal
     }
 
-    val setFlagsRule = SetFlagsRule()
+    @get:Rule
     val screenshotRule = InputScreenshotTestRule(
             emulationSpec,
             "frameworks/base/tests/InputScreenshotTest/assets"
     )
 
-    @get:Rule
-    val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule)
-
     @Test
     fun test() {
-        setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG)
         screenshotRule.screenshotTest("layout-preview-ansi") {
             context: Context -> LayoutPreview.createLayoutPreview(
                 context,
@@ -66,5 +59,4 @@
             )
         }
     }
-
 }
\ No newline at end of file
diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt
index ab7bb4e..ddad6de 100644
--- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt
+++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt
@@ -17,14 +17,8 @@
 package com.android.input.screenshot
 
 import android.content.Context
-import android.hardware.input.KeyboardLayout
-import android.os.LocaleList
-import android.platform.test.flag.junit.SetFlagsRule
-import com.android.hardware.input.Flags
-import java.util.Locale
 import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
@@ -39,21 +33,16 @@
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
 
-    val setFlagsRule = SetFlagsRule()
+    @get:Rule
     val screenshotRule = InputScreenshotTestRule(
             emulationSpec,
             "frameworks/base/tests/InputScreenshotTest/assets"
     )
 
-    @get:Rule
-    val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule)
-
     @Test
     fun test() {
-        setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG)
         screenshotRule.screenshotTest("layout-preview") {
             context: Context -> LayoutPreview.createLayoutPreview(context, null)
         }
     }
-
 }
diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt
index 5231c14..8a8e4f0 100644
--- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt
+++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt
@@ -19,12 +19,9 @@
 import android.content.Context
 import android.hardware.input.KeyboardLayout
 import android.os.LocaleList
-import android.platform.test.flag.junit.SetFlagsRule
-import com.android.hardware.input.Flags
 import java.util.Locale
 import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import platform.test.screenshot.DeviceEmulationSpec
@@ -38,18 +35,14 @@
         fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal
     }
 
-    val setFlagsRule = SetFlagsRule()
+    @get:Rule
     val screenshotRule = InputScreenshotTestRule(
             emulationSpec,
             "frameworks/base/tests/InputScreenshotTest/assets"
     )
 
-    @get:Rule
-    val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule)
-
     @Test
     fun test() {
-        setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG)
         screenshotRule.screenshotTest("layout-preview-jis") {
             context: Context -> LayoutPreview.createLayoutPreview(
                 context,
@@ -66,5 +59,4 @@
             )
         }
     }
-
 }
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index f00a6ca..2031556 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -54,9 +54,7 @@
 void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value,
                               uint32_t flags) {
   auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
-    if (value) {
-      *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
-    }
+    *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
     return true;
   };
 
@@ -67,9 +65,7 @@
 void Command::AddRequiredFlagList(StringPiece name, StringPiece description,
                                   std::vector<std::string>* value, uint32_t flags) {
   auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
-    if (value) {
-      value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
-    }
+    value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
     return true;
   };
 
@@ -80,9 +76,7 @@
 void Command::AddOptionalFlag(StringPiece name, StringPiece description,
                               std::optional<std::string>* value, uint32_t flags) {
   auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
-    if (value) {
-      *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
-    }
+    *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
     return true;
   };
 
@@ -93,9 +87,7 @@
 void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
                                   std::vector<std::string>* value, uint32_t flags) {
   auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
-    if (value) {
-      value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
-    }
+    value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
     return true;
   };
 
@@ -106,9 +98,7 @@
 void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
                                   std::unordered_set<std::string>* value) {
   auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
-    if (value) {
-      value->emplace(arg);
-    }
+    value->emplace(arg);
     return true;
   };
 
@@ -118,9 +108,7 @@
 
 void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) {
   auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
-    if (value) {
-      *value = true;
-    }
+    *value = true;
     return true;
   };
 
diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp
index ad167c9..2a3cb2a 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -159,22 +159,4 @@
   ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr));
 }
 
-TEST(CommandTest, OptionsWithNullptrToAcceptValues) {
-  TestCommand command;
-  command.AddRequiredFlag("--rflag", "", nullptr);
-  command.AddRequiredFlagList("--rlflag", "", nullptr);
-  command.AddOptionalFlag("--oflag", "", nullptr);
-  command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr);
-  command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr);
-  command.AddOptionalSwitch("--switch", "", nullptr);
-
-  ASSERT_EQ(0, command.Execute({
-    "--rflag"s, "1"s,
-    "--rlflag"s, "1"s,
-    "--oflag"s, "1"s,
-    "--olflag"s, "1"s,
-    "--olflag2"s, "1"s,
-    "--switch"s}, &std::cerr));
-}
-
 }  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index 060bc5f..6c3eae1 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -425,6 +425,9 @@
                                     << output_format_.value());
     return 1;
   }
+  if (enable_sparse_encoding_) {
+    table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled;
+  }
   if (force_sparse_encoding_) {
     table_flattener_options_.sparse_entries = SparseEntriesMode::Forced;
   }
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 98c8f5f..9452e58 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -36,9 +36,11 @@
         kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_);
     AddOptionalSwitch(
         "--enable-sparse-encoding",
-        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
-        "enabled if minSdk of the APK is >= 32.",
-        nullptr);
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
+        "the APK is O+",
+        &enable_sparse_encoding_);
     AddOptionalSwitch("--force-sparse-encoding",
                       "Enables encoding sparse entries using a binary search tree.\n"
                       "This decreases APK size at the cost of resource retrieval performance.\n"
@@ -85,6 +87,7 @@
   std::string output_path_;
   std::optional<std::string> output_format_;
   bool verbose_ = false;
+  bool enable_sparse_encoding_ = false;
   bool force_sparse_encoding_ = false;
   bool enable_compact_entries_ = false;
   std::optional<std::string> resources_config_path_;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 4718fbf..ff4d8ef 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2505,6 +2505,9 @@
                 << "the --merge-only flag can be only used when building a static library");
     return 1;
   }
+  if (options_.use_sparse_encoding) {
+    options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
+  }
 
   // The default build type.
   context.SetPackageType(PackageType::kApp);
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index b5bd905..2f17853 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -75,6 +75,7 @@
   bool no_resource_removal = false;
   bool no_xml_namespaces = false;
   bool do_not_compress_anything = false;
+  bool use_sparse_encoding = false;
   std::unordered_set<std::string> extensions_to_not_compress;
   std::optional<std::regex> regex_to_not_compress;
   FeatureFlagValues feature_flag_values;
@@ -162,11 +163,9 @@
     AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
             "defaults. Use this only when building runtime resource overlay packages.",
         &options_.no_resource_removal);
-    AddOptionalSwitch(
-        "--enable-sparse-encoding",
-        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
-        "enabled if minSdk of the APK is >= 32.",
-        nullptr);
+    AddOptionalSwitch("--enable-sparse-encoding",
+                      "This decreases APK size at the cost of resource retrieval performance.",
+                      &options_.use_sparse_encoding);
     AddOptionalSwitch("--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
         &options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index f218307..762441e 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -406,6 +406,9 @@
     return 1;
   }
 
+  if (options_.enable_sparse_encoding) {
+    options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
+  }
   if (options_.force_sparse_encoding) {
     options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced;
   }
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index e3af584..012b0f2 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -61,6 +61,9 @@
   // TODO(b/246489170): keep the old option and format until transform to the new one
   std::optional<std::string> shortened_paths_map_path;
 
+  // Whether sparse encoding should be used for O+ resources.
+  bool enable_sparse_encoding = false;
+
   // Whether sparse encoding should be used for all resources.
   bool force_sparse_encoding = false;
 
@@ -103,9 +106,11 @@
         &kept_artifacts_);
     AddOptionalSwitch(
         "--enable-sparse-encoding",
-        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
-        "enabled if minSdk of the APK is >= 32.",
-        nullptr);
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
+        "the APK is O+",
+        &options_.enable_sparse_encoding);
     AddOptionalSwitch("--force-sparse-encoding",
                       "Enables encoding sparse entries using a binary search tree.\n"
                       "This decreases APK size at the cost of resource retrieval performance.\n"
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index b8ac792..1a82021 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -201,7 +201,7 @@
         (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
       // Sparse encode if forced or sdk version is not set in context and config.
     } else {
-      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32).
+      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
       sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
     }
 
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index f1c4c35..0633bc81 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -37,7 +37,8 @@
 enum class SparseEntriesMode {
   // Disables sparse encoding for entries.
   Disabled,
-  // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2).
+  // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less
+  // than O only applies sparse encoding for resource configuration available on O+.
   Enabled,
   // Enables sparse encoding for all entries regardless of minSdk.
   Forced,
@@ -46,7 +47,7 @@
 struct TableFlattenerOptions {
   // When enabled, types for configurations with a sparse set of entries are encoded
   // as a sparse map of entry ID and offset to actual data.
-  SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled;
+  SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled;
 
   // When true, use compact entries for simple data
   bool use_compact_entries = false;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index e3d589e..0f11685 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -337,13 +337,13 @@
   auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
 
   TableFlattenerOptions options;
-  options.sparse_entries = SparseEntriesMode::Disabled;
+  options.sparse_entries = SparseEntriesMode::Enabled;
 
   std::string no_sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
 
   std::string sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
@@ -421,13 +421,13 @@
   auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
 
   TableFlattenerOptions options;
-  options.sparse_entries = SparseEntriesMode::Disabled;
+  options.sparse_entries = SparseEntriesMode::Enabled;
 
   std::string no_sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
 
   std::string sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 664d841..5c3dfdc 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -3,8 +3,6 @@
 ## Version 2.20
 - Too many features, bug fixes, and improvements to list since the last minor version update in
   2017. This README will be updated more frequently in the future.
-- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The
-  `--enable-sparse-encoding` flag still exists, but is a no-op.
 
 ## Version 2.19
 - Added navigation resource type.