Merge "Batch calls to the Metadata Syncer" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ca9789..dd919ca 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -93,6 +93,7 @@
         "com.android.media.flags.performance-aconfig-java",
         "com.android.media.flags.projection-aconfig-java",
         "com.android.net.thread.platform.flags-aconfig-java",
+        "com.android.ranging.flags.ranging-aconfig-java",
         "com.android.server.contextualsearch.flags-java",
         "com.android.server.flags.services-aconfig-java",
         "com.android.text.flags-aconfig-java",
@@ -1549,6 +1550,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// Ranging
+java_aconfig_library {
+    name: "com.android.ranging.flags.ranging-aconfig-java",
+    aconfig_declarations: "ranging_aconfig_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // System Server
 aconfig_declarations {
     name: "android.systemserver.flags-aconfig",
diff --git a/api/Android.bp b/api/Android.bp
index 533f9f6..3f2316f 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -102,6 +102,11 @@
             "framework-crashrecovery",
         ],
         default: [],
+    }) + select(release_flag("RELEASE_RANGING_STACK"), {
+        true: [
+            "framework-ranging",
+        ],
+        default: [],
     }),
     system_server_classpath: [
         "service-art",
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index df45862..8447a7f 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -387,6 +387,12 @@
     field public static final int DEVICE_INITIAL_SDK_INT;
   }
 
+  public class Handler {
+    method @FlaggedApi("android.os.mainline_vcn_platform_api") public final boolean hasMessagesOrCallbacks();
+    method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeCallbacksAndEqualMessages(@Nullable Object);
+    method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeEqualMessages(int, @Nullable Object);
+  }
+
   public class IpcDataCache<Query, Result> {
     ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
     method public void disableForCurrentProcess();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7a8e829..b2a49e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3835,6 +3835,7 @@
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
     field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
     field public static final String PERMISSION_SERVICE = "permission";
+    field @FlaggedApi("com.android.ranging.flags.ranging_stack_enabled") public static final String RANGING_SERVICE = "ranging";
     field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
     field public static final String ROLLBACK_SERVICE = "rollback";
     field public static final String SAFETY_CENTER_SERVICE = "safety_center";
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 5db79fe..3bc3a93 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -255,6 +255,7 @@
 import libcore.io.IoUtils;
 import libcore.io.Os;
 import libcore.net.event.NetworkEventDispatcher;
+import libcore.util.NativeAllocationRegistry;
 
 import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
 
@@ -1610,6 +1611,32 @@
         }
 
         @NeverCompile
+        private void dumpMemInfoNativeAllocations(PrintWriter pw) {
+            pw.println(" ");
+            pw.println(" Native Allocations");
+            printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "Count", "", "Total(kB)");
+            printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "------", "", "------");
+
+            for (NativeAllocationRegistry.Metrics m : NativeAllocationRegistry.getMetrics()) {
+                // group into 3 major categories: Bitmap, HardwareBuffer and Other
+                final String className = switch (m.getClassName()) {
+                    case "android.graphics.Bitmap" -> "Bitmap";
+                    case "android.hardware.HardwareBuffer" -> "HardwareBuffer";
+                    default -> "Other";
+                };
+
+                if (m.getMallocedCount() != 0 || m.getMallocedBytes() != 0) {
+                    printRow(pw, TWO_COUNT_COLUMNS, className + " (malloced):",
+                        m.getMallocedCount(), "", m.getMallocedBytes() / 1024);
+                }
+                if (m.getNonmallocedCount() != 0 || m.getNonmallocedBytes() != 0) {
+                    printRow(pw, TWO_COUNT_COLUMNS, className + " (nonmalloced):",
+                        m.getNonmallocedCount(), "", m.getNonmallocedBytes() / 1024);
+                }
+            }
+        }
+
+        @NeverCompile
         private void dumpMemInfo(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
                 boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
                 boolean dumpUnreachable, boolean dumpAllocatorStats) {
@@ -1707,6 +1734,10 @@
             printRow(pw, TWO_COUNT_COLUMNS, "Death Recipients:", binderDeathObjectCount,
                     "WebViews:", webviewInstanceCount);
 
+            if (com.android.libcore.Flags.nativeMetrics()) {
+                dumpMemInfoNativeAllocations(pw);
+            }
+
             // SQLite mem info
             pw.println(" ");
             pw.println(" SQL");
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index dbf9afd..ed6b851 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+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;
@@ -817,7 +818,7 @@
     private final static PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>
             mHasSystemFeatureCache =
             new PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>(
-                256, "cache_key.has_system_feature") {
+                256, createSystemCacheKey("has_system_feature")) {
                 @Override
                 public Boolean recompute(HasSystemFeatureQuery query) {
                     try {
@@ -1127,7 +1128,7 @@
     }
 
     private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
-            "cache_key.get_packages_for_uid";
+            createSystemCacheKey("get_packages_for_uid");
     private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
             mGetPackagesForUidCache =
             new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 0c786cb..0e761fc 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -283,6 +284,12 @@
      */
 
     /**
+     * The well-known key prefix.
+     * @hide
+     */
+    private static final String CACHE_KEY_PREFIX = "cache_key";
+
+    /**
      * The module used for unit tests and cts tests.  It is expected that no process in
      * the system has permissions to write properties with this module.
      * @hide
@@ -366,7 +373,44 @@
             }
         }
 
-        return "cache_key." + module + "." + new String(suffix);
+        return CACHE_KEY_PREFIX + "." + module + "." + new String(suffix);
+    }
+
+    /**
+     * All legal keys start with one of the following strings.
+     */
+    private static final String[] sValidKeyPrefix = {
+        CACHE_KEY_PREFIX + "." + MODULE_SYSTEM + ".",
+        CACHE_KEY_PREFIX + "." + MODULE_BLUETOOTH + ".",
+        CACHE_KEY_PREFIX + "." + MODULE_TELEPHONY + ".",
+        CACHE_KEY_PREFIX + "." + MODULE_TEST + ".",
+    };
+
+    /**
+     * Verify that the property name conforms to the standard.  Log a warning if this is not true.
+     * Note that this is done once in the cache constructor; it does not have to be very fast.
+     */
+    private void validateCacheKey(String name) {
+        if (Build.IS_USER) {
+            // Do not bother checking keys in user builds.  The keys will have been tested in
+            // eng/userdebug builds already.
+            return;
+        }
+        for (int i = 0; i < sValidKeyPrefix.length; i++) {
+            if (name.startsWith(sValidKeyPrefix[i])) return;
+        }
+        Log.w(TAG, "invalid cache name: " + name);
+    }
+
+    /**
+     * Create a cache key for the system module.  The parameter is the API name.  This reduces
+     * some of the boilerplate in system caches.  It is not needed in other modules because other
+     * modules must use the {@link IpcDataCache} interfaces.
+     * @hide
+     */
+    @NonNull
+    public static String createSystemCacheKey(@NonNull String api) {
+        return createPropertyName(MODULE_SYSTEM, api);
     }
 
     /**
@@ -561,6 +605,7 @@
     public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName,
             @NonNull String cacheName) {
         mPropertyName = propertyName;
+        validateCacheKey(mPropertyName);
         mCacheName = cacheName;
         mMaxEntries = maxEntries;
         mComputer = new DefaultComputer<>(this);
@@ -584,6 +629,7 @@
     public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api,
             @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
         mPropertyName = createPropertyName(module, api);
+        validateCacheKey(mPropertyName);
         mCacheName = cacheName;
         mMaxEntries = maxEntries;
         mComputer = computer;
diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java
index 7948cec..db663f8 100644
--- a/core/java/android/app/compat/ChangeIdStateCache.java
+++ b/core/java/android/app/compat/ChangeIdStateCache.java
@@ -16,6 +16,8 @@
 
 package android.app.compat;
 
+import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
+
 import android.annotation.NonNull;
 import android.app.PropertyInvalidatedCache;
 import android.content.Context;
@@ -31,7 +33,7 @@
  */
 public final class ChangeIdStateCache
         extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> {
-    private static final String CACHE_KEY = "cache_key.is_compat_change_enabled";
+    private static final String CACHE_KEY = createSystemCacheKey("is_compat_change_enabled");
     private static final int MAX_ENTRIES = 2048;
     private static boolean sDisabled = false;
     private volatile IPlatformCompat mPlatformCompat;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 12c5d07..91f7a8b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4324,6 +4324,7 @@
             SECURITY_STATE_SERVICE,
            //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE,
             CONTACT_KEYS_SERVICE,
+            RANGING_SERVICE,
 
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -6402,6 +6403,17 @@
 
     /**
      * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.ranging.RangingManager}.
+     *
+     * @see #getSystemService(String)
+     * @hide
+     */
+    @FlaggedApi(com.android.ranging.flags.Flags.FLAG_RANGING_STACK_ENABLED)
+    @SystemApi
+    public static final String RANGING_SERVICE = "ranging";
+
+    /**
+     * Use with {@link #getSystemService(String)} to retrieve a
      * {@link android.app.DreamManager} for controlling Dream states.
      *
      * @see #getSystemService(String)
diff --git a/core/java/android/content/pm/Android.bp b/core/java/android/content/pm/Android.bp
new file mode 100644
index 0000000..057b5da
--- /dev/null
+++ b/core/java/android/content/pm/Android.bp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package {
+    default_team: "trendy_team_framework_android_packages",
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "framework-pm-sources",
+    srcs: [
+        "**/*.java",
+        "**/*.aidl",
+    ],
+    exclude_srcs: [
+        "dex/**/*.java",
+        "overlay/**/*.java",
+        "permission/**/*.java",
+    ],
+    visibility: ["//frameworks/base"],
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index d40b2e3..2162792 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -818,7 +818,7 @@
             }
 
             boolean hasConcurrentStreams =
-                    CameraManagerGlobal.get().cameraIdHasConcurrentStreamsLocked(cameraId,
+                    CameraManagerGlobal.get().cameraIdHasConcurrentStreams(cameraId,
                             mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
             metadata.setHasMandatoryConcurrentStreams(hasConcurrentStreams);
 
@@ -2629,24 +2629,26 @@
          * @return Whether the camera device was found in the set of combinations returned by
          *         getConcurrentCameraIds
          */
-        public boolean cameraIdHasConcurrentStreamsLocked(String cameraId, int deviceId,
+        public boolean cameraIdHasConcurrentStreams(String cameraId, int deviceId,
                 int devicePolicy) {
-            DeviceCameraInfo info = new DeviceCameraInfo(cameraId,
-                    devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId);
-            if (!mDeviceStatus.containsKey(info)) {
-                // physical camera ids aren't advertised in concurrent camera id combinations.
-                if (DEBUG) {
-                    Log.v(TAG, " physical camera id " + cameraId + " is hidden." +
-                            " Available logical camera ids : " + mDeviceStatus);
+            synchronized (mLock) {
+                DeviceCameraInfo info = new DeviceCameraInfo(cameraId,
+                        devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId);
+                if (!mDeviceStatus.containsKey(info)) {
+                    // physical camera ids aren't advertised in concurrent camera id combinations.
+                    if (DEBUG) {
+                        Log.v(TAG, " physical camera id " + cameraId + " is hidden."
+                                + " Available logical camera ids : " + mDeviceStatus);
+                    }
+                    return false;
+                }
+                for (Set<DeviceCameraInfo> comb : mConcurrentCameraIdCombinations) {
+                    if (comb.contains(info)) {
+                        return true;
+                    }
                 }
                 return false;
             }
-            for (Set<DeviceCameraInfo> comb : mConcurrentCameraIdCombinations) {
-                if (comb.contains(info)) {
-                    return true;
-                }
-            }
-            return false;
         }
 
         public void setTorchMode(
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 9612a53..7185719 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1445,7 +1445,7 @@
      * system's display configuration.
      */
     public static final String CACHE_KEY_DISPLAY_INFO_PROPERTY =
-            "cache_key.display_info";
+            PropertyInvalidatedCache.createSystemCacheKey("display_info");
 
     /**
      * Invalidates the contents of the display info cache for all applications. Can only
diff --git a/core/java/android/hardware/fingerprint/FingerprintCallback.java b/core/java/android/hardware/fingerprint/FingerprintCallback.java
index e4fbe6e..24e9f9d 100644
--- a/core/java/android/hardware/fingerprint/FingerprintCallback.java
+++ b/core/java/android/hardware/fingerprint/FingerprintCallback.java
@@ -189,7 +189,7 @@
             mEnrollmentCallback.onAcquired(acquireInfo == FINGERPRINT_ACQUIRED_GOOD);
         }
         final String msg = getAcquiredString(context, acquireInfo, vendorCode);
-        if (msg == null) {
+        if (msg == null || msg.isEmpty()) {
             return;
         }
         // emulate HAL 2.1 behavior and send real acquiredInfo
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 983bbc3..75683f6 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -138,3 +138,10 @@
   description: "Controls whether external mouse vertical scrolling can be reversed"
   bug: "352598211"
 }
+
+flag {
+  name: "mouse_swap_primary_button"
+  namespace: "input"
+  description: "Controls whether the connected mice's primary buttons, left and right, can be swapped."
+  bug: "352598211"
+}
diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java
index 80f39bf..d0828c3 100644
--- a/core/java/android/os/Handler.java
+++ b/core/java/android/os/Handler.java
@@ -16,8 +16,10 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.util.Log;
 import android.util.Printer;
@@ -819,16 +821,25 @@
     }
 
     /**
+     * WARNING: This API is dangerous because if the implementation
+     * of equals() is broken, it would delete unrelated events. For example,
+     * if object.equals() always returns true, it'd remove all messages.
+     *
+     * For this reason, never expose this API to non-platform code. i.e.
+     * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+     *
      * Remove any pending posts of messages with code 'what' and whose obj is
      * 'object' that are in the message queue.  If <var>object</var> is null,
      * all messages will be removed.
-     * <p>
-     * Similar to {@link #removeMessages(int, Object)} but uses object equality
+     *
+     * <p>Similar to {@link #removeMessages(int, Object)} but uses object equality
      * ({@link Object#equals(Object)}) instead of reference equality (==) in
      * determining whether object is the message's obj'.
      *
      *@hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
     public final void removeEqualMessages(int what, @Nullable Object object) {
         mQueue.removeEqualMessages(this, what, disallowNullArgumentIfShared(object));
     }
@@ -843,12 +854,25 @@
     }
 
     /**
+     * WARNING: This API is dangerous because if the implementation
+     * of equals() is broken, it would delete unrelated events. For example,
+     * if object.equals() always returns true, it'd remove all messages.
+     *
+     * For this reason, never expose this API to non-platform code. i.e.
+     * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+     *
      * Remove any pending posts of callbacks and sent messages whose
      * <var>obj</var> is <var>token</var>.  If <var>token</var> is null,
      * all callbacks and messages will be removed.
      *
+     * <p>Similar to {@link #removeCallbacksAndMessages(Object)} but uses object
+     * equality ({@link Object#equals(Object)}) instead of reference equality (==) in
+     * determining whether object is the message's obj'.
+     *
      *@hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
     public final void removeCallbacksAndEqualMessages(@Nullable Object token) {
         mQueue.removeCallbacksAndEqualMessages(this, disallowNullArgumentIfShared(token));
     }
@@ -864,6 +888,8 @@
      * Return whether there are any messages or callbacks currently scheduled on this handler.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
     public final boolean hasMessagesOrCallbacks() {
         return mQueue.hasMessages(this);
     }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 026013c..e4c12b6 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1144,9 +1144,10 @@
     }
 
     private static final String CACHE_KEY_IS_POWER_SAVE_MODE_PROPERTY =
-            "cache_key.is_power_save_mode";
+            PropertyInvalidatedCache.createSystemCacheKey("is_power_save_mode");
 
-    private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY = "cache_key.is_interactive";
+    private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY =
+            PropertyInvalidatedCache.createSystemCacheKey("is_interactive");
 
     private static final int MAX_CACHE_ENTRIES = 1;
 
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 738d129..f670601 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -216,3 +216,11 @@
     bug: "346294653"
     is_exported: true
 }
+
+flag {
+     name: "mainline_vcn_platform_api"
+     namespace: "vcn"
+     description: "Expose platform APIs to mainline VCN"
+     is_exported: true
+     bug: "366598445"
+}
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 7e51cb0..e98397d 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1796,7 +1796,8 @@
     }
 
     /** @hide */
-    public static final String CACHE_KEY_PACKAGE_INFO = "cache_key.package_info";
+    public static final String CACHE_KEY_PACKAGE_INFO =
+            PropertyInvalidatedCache.createSystemCacheKey("package_info");
 
     /** @hide */
     private static final PropertyInvalidatedCache<PermissionQuery, Integer> sPermissionCache =
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index 3cfde87..8bb4c52 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -187,6 +187,7 @@
 
         /** If non-null, requires the change to specifically have or not-have a custom animation. */
         public Boolean mCustomAnimation = null;
+        public IBinder mTaskFragmentToken = null;
 
         public Requirement() {
         }
@@ -204,12 +205,19 @@
             // 0: null, 1: false, 2: true
             final int customAnimRaw = in.readInt();
             mCustomAnimation = customAnimRaw == 0 ? null : Boolean.valueOf(customAnimRaw == 2);
+            mTaskFragmentToken = in.readStrongBinder();
         }
 
         /** Go through changes and find if at-least one change matches this filter */
         boolean matches(@NonNull TransitionInfo info) {
             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                 final TransitionInfo.Change change = info.getChanges().get(i);
+
+                if (mTaskFragmentToken != null
+                        && !mTaskFragmentToken.equals(change.getTaskFragmentToken())) {
+                    continue;
+                }
+
                 if (mMustBeIndependent && !TransitionInfo.isIndependent(change, info)) {
                     // Only look at independent animating windows.
                     continue;
@@ -313,6 +321,7 @@
             dest.writeStrongBinder(mLaunchCookie);
             int customAnimRaw = mCustomAnimation == null ? 0 : (mCustomAnimation ? 2 : 1);
             dest.writeInt(customAnimRaw);
+            dest.writeStrongBinder(mTaskFragmentToken);
         }
 
         @NonNull
@@ -357,6 +366,9 @@
             if (mCustomAnimation != null) {
                 out.append(" customAnim=").append(mCustomAnimation.booleanValue());
             }
+            if (mTaskFragmentToken != null) {
+                out.append(" taskFragmentToken=").append(mTaskFragmentToken);
+            }
             out.append("}");
             return out.toString();
         }
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index ec79f94..14505f5 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -49,6 +49,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Surface;
@@ -681,6 +682,7 @@
         private float mSnapshotLuma;
         private ComponentName mActivityComponent = null;
         private AnimationOptions mAnimationOptions = null;
+        private IBinder mTaskFragmentToken = null;
 
         public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
             mContainer = container;
@@ -712,6 +714,7 @@
             mSnapshotLuma = in.readFloat();
             mActivityComponent = in.readTypedObject(ComponentName.CREATOR);
             mAnimationOptions = in.readTypedObject(AnimationOptions.CREATOR);
+            mTaskFragmentToken = in.readStrongBinder();
         }
 
         private Change localRemoteCopy() {
@@ -737,6 +740,7 @@
             out.mSnapshotLuma = mSnapshotLuma;
             out.mActivityComponent = mActivityComponent;
             out.mAnimationOptions = mAnimationOptions;
+            out.mTaskFragmentToken = mTaskFragmentToken;
             return out;
         }
 
@@ -854,6 +858,14 @@
             mAnimationOptions = options;
         }
 
+        /**
+         * Sets the client-defined TaskFragment token. Only set this if the window is a
+         * client-organized TaskFragment.
+         */
+        public void setTaskFragmentToken(@Nullable IBinder token) {
+            mTaskFragmentToken = token;
+        }
+
         /** @return the container that is changing. May be null if non-remotable (eg. activity) */
         @Nullable
         public WindowContainerToken getContainer() {
@@ -1009,6 +1021,15 @@
             return mAnimationOptions;
         }
 
+        /**
+         * Returns the client-defined TaskFragment token. {@code null} if this window is not a
+         * client-organized TaskFragment.
+         */
+        @Nullable
+        public IBinder getTaskFragmentToken() {
+            return mTaskFragmentToken;
+        }
+
         /** @hide */
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -1035,6 +1056,7 @@
             dest.writeFloat(mSnapshotLuma);
             dest.writeTypedObject(mActivityComponent, flags);
             dest.writeTypedObject(mAnimationOptions, flags);
+            dest.writeStrongBinder(mTaskFragmentToken);
         }
 
         @NonNull
@@ -1110,6 +1132,9 @@
             if (mAnimationOptions != null) {
                 sb.append(" opt=").append(mAnimationOptions);
             }
+            if (mTaskFragmentToken != null) {
+                sb.append(" taskFragmentToken=").append(mTaskFragmentToken);
+            }
             sb.append('}');
             return sb.toString();
         }
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index fe8b818..a14461a 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -17,7 +17,7 @@
 <resources>
     <!-- Determines whether the shell features all run on another thread. This is to be overrided
          by the resources of the app using the Shell library. -->
-    <bool name="config_enableShellMainThread">false</bool>
+    <bool name="config_enableShellMainThread">true</bool>
 
     <!-- Determines whether to register the shell task organizer on init.
          TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
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 2f4d77b..584f272 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
@@ -75,6 +75,7 @@
 import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
 import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
 import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
 import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
@@ -141,7 +142,7 @@
         includes = {
                 WMShellBaseModule.class,
                 PipModule.class,
-                ShellBackAnimationModule.class,
+                ShellBackAnimationModule.class
         })
 public abstract class WMShellModule {
 
@@ -247,6 +248,7 @@
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
             WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -272,6 +274,7 @@
                     assistContentRequester,
                     multiInstanceHelper,
                     desktopTasksLimiter,
+                    windowDecorCaptionHandleRepository,
                     desktopActivityOrientationHandler,
                     windowDecorViewHostSupplier);
         }
@@ -780,6 +783,12 @@
 
     @WMSingleton
     @Provides
+    static WindowDecorCaptionHandleRepository provideAppHandleRepository() {
+        return new WindowDecorCaptionHandleRepository();
+    }
+
+    @WMSingleton
+    @Provides
     static AppHandleEducationController provideAppHandleEducationController(
             AppHandleEducationFilter appHandleEducationFilter,
             ShellTaskOrganizer shellTaskOrganizer,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
new file mode 100644
index 0000000..7ae5370
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Repository to observe caption state. */
+class WindowDecorCaptionHandleRepository {
+  private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
+  /** Observer for app handle state changes. */
+  val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow
+
+  /** Notifies [captionStateFlow] if there is a change to caption state. */
+  fun notifyCaptionChanged(captionState: CaptionState) {
+    _captionStateFlow.value = captionState
+  }
+}
+
+/**
+ * Represents the current status of the caption.
+ *
+ * It can be one of three options:
+ * * [AppHandle]: Indicating that there is at least one visible app handle on the screen.
+ * * [AppHeader]: Indicating that there is at least one visible app chip on the screen.
+ * * [NoCaption]: Signifying that no caption handle is currently visible on the device.
+ */
+sealed class CaptionState {
+  data class AppHandle(
+      val runningTaskInfo: RunningTaskInfo,
+      val isHandleMenuExpanded: Boolean,
+      val globalAppHandleBounds: Rect
+  ) : CaptionState()
+
+  data class AppHeader(
+      val runningTaskInfo: RunningTaskInfo,
+      val isHeaderMenuExpanded: Boolean,
+      val globalAppChipBounds: Rect
+  ) : CaptionState()
+
+  data object NoCaption : CaptionState()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 82fbfad..5710af6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -204,8 +204,7 @@
     @NonNull
     @Override
     public Rect getFloatingBoundsOnScreen() {
-        return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty()
-                ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds();
+        return getBounds();
     }
 
     @NonNull
@@ -616,7 +615,7 @@
             cancelPhysicsAnimation();
         }
 
-        setAnimatingToBounds(new Rect(
+        mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(new Rect(
                 (int) toX,
                 (int) toY,
                 (int) toX + getBounds().width(),
@@ -660,6 +659,9 @@
             // All motion operations have actually finished.
             mPipBoundsState.setBounds(
                     mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+            // Notifies the floating coordinator that we moved, so we return these bounds from
+            // {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
+            mFloatingContentCoordinator.onContentMoved(this);
             mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
             if (!mDismissalPending) {
                 // do not schedule resize if PiP is dismissing, which may cause app re-open to
@@ -674,16 +676,6 @@
     }
 
     /**
-     * Notifies the floating coordinator that we're moving, and sets the animating to bounds so
-     * we return these bounds from
-     * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
-     */
-    private void setAnimatingToBounds(Rect bounds) {
-        mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds);
-        mFloatingContentCoordinator.onContentMoved(this);
-    }
-
-    /**
      * Directly resizes the PiP to the given {@param bounds}.
      */
     private void resizePipUnchecked(Rect toBounds) {
@@ -712,7 +704,7 @@
         // This is so all the proper callbacks are performed.
         mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration,
                 TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */);
-        setAnimatingToBounds(toBounds);
+        mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(toBounds);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index caac2f6..7ea0bd6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -109,6 +109,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
 import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
 import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -164,7 +165,9 @@
     private final InputManager mInputManager;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final MultiInstanceHelper mMultiInstanceHelper;
+    private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
     private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
+    private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
     private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier;
     private boolean mTransitionDragActive;
 
@@ -234,6 +237,7 @@
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
             WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
         this(
@@ -259,10 +263,12 @@
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new,
+                new AppHeaderViewHolder.Factory(),
                 rootTaskDisplayAreaOrganizer,
                 new SparseArray<>(),
                 interactionJankMonitor,
                 desktopTasksLimiter,
+                windowDecorCaptionHandleRepository,
                 activityOrientationChangeHandler,
                 new TaskPositionerFactory());
     }
@@ -291,10 +297,12 @@
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory,
+            AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId,
             InteractionJankMonitor interactionJankMonitor,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
             TaskPositionerFactory taskPositionerFactory) {
         mContext = context;
@@ -317,6 +325,7 @@
         mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
         mInputMonitorFactory = inputMonitorFactory;
         mTransactionFactory = transactionFactory;
+        mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
         mGenericLinksParser = genericLinksParser;
         mInputManager = mContext.getSystemService(InputManager.class);
@@ -325,6 +334,7 @@
                 com.android.internal.R.string.config_systemUi);
         mInteractionJankMonitor = interactionJankMonitor;
         mDesktopTasksLimiter = desktopTasksLimiter;
+        mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
         mActivityOrientationChangeHandler = activityOrientationChangeHandler;
         mAssistContentRequester = assistContentRequester;
         mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
@@ -1377,10 +1387,12 @@
                         mBgExecutor,
                         mMainChoreographer,
                         mSyncQueue,
+                        mAppHeaderViewHolderFactory,
                         mRootTaskDisplayAreaOrganizer,
                         mGenericLinksParser,
                         mAssistContentRequester,
                         mMultiInstanceHelper,
+                        mWindowDecorCaptionHandleRepository,
                         mWindowDecorViewHostSupplier);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 16036be..10e4a39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -28,6 +28,7 @@
 import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
 
 import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
 import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
@@ -85,6 +86,8 @@
 import com.android.wm.shell.common.MultiInstanceHelper;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -165,6 +168,7 @@
 
     private ExclusionRegionListener mExclusionRegionListener;
 
+    private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private final MaximizeMenuFactory mMaximizeMenuFactory;
     private final HandleMenuFactory mHandleMenuFactory;
@@ -181,6 +185,7 @@
     private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
     private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
     private final MultiInstanceHelper mMultiInstanceHelper;
+    private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
 
     DesktopModeWindowDecoration(
             Context context,
@@ -194,20 +199,24 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
+            AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             AppToWebGenericLinksParser genericLinksParser,
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
         this (context, userContext, displayController, splitScreenController, taskOrganizer,
                 taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
-                rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester,
+                appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
+                assistContentRequester,
                 SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                 WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(
                         context.getSystemService(WindowManager.class)),
                 new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier,
                 DefaultMaximizeMenuFactory.INSTANCE,
-                DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper);
+                DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper,
+                windowDecorCaptionHandleRepository);
     }
 
     DesktopModeWindowDecoration(
@@ -222,6 +231,7 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
+            AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             AppToWebGenericLinksParser genericLinksParser,
             AssistContentRequester assistContentRequester,
@@ -234,7 +244,8 @@
             WindowDecorViewHostSupplier windowDecorViewHostSupplier,
             MaximizeMenuFactory maximizeMenuFactory,
             HandleMenuFactory handleMenuFactory,
-            MultiInstanceHelper multiInstanceHelper) {
+            MultiInstanceHelper multiInstanceHelper,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
         super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -244,6 +255,7 @@
         mBgExecutor = bgExecutor;
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
+        mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
         mGenericLinksParser = genericLinksParser;
         mAssistContentRequester = assistContentRequester;
@@ -251,6 +263,7 @@
         mHandleMenuFactory = handleMenuFactory;
         mMultiInstanceHelper = multiInstanceHelper;
         mWindowManagerWrapper = windowManagerWrapper;
+        mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
     }
 
     /**
@@ -383,6 +396,9 @@
         if (mResult.mRootView == null) {
             // This means something blocks the window decor from showing, e.g. the task is hidden.
             // Nothing is set up in this case including the decoration surface.
+            if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+                notifyNoCaptionHandle();
+            }
             disposeStatusBarInputLayer();
             Trace.endSection(); // DesktopModeWindowDecoration#relayout
             return;
@@ -398,6 +414,9 @@
             position.set(determineHandlePosition());
         }
         Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData");
+        if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+            notifyCaptionStateChanged();
+        }
         mWindowDecorViewHolder.bindData(mTaskInfo,
                 position,
                 mResult.mCaptionWidth,
@@ -407,6 +426,7 @@
 
         if (!mTaskInfo.isFocused) {
             closeHandleMenu();
+            closeManageWindowsMenu();
             closeMaximizeMenu();
         }
         updateDragResizeListener(oldDecorationSurface);
@@ -507,6 +527,67 @@
         return taskInfo.isFreeform() && taskInfo.isResizeable;
     }
 
+    private void notifyCaptionStateChanged() {
+        // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode.
+        if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+            return;
+        }
+        if (!isCaptionVisible()) {
+            notifyNoCaptionHandle();
+        } else if (isAppHandle(mWindowDecorViewHolder)) {
+            // App handle is visible since `mWindowDecorViewHolder` is of type
+            // [AppHandleViewHolder].
+            final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo,
+                    isHandleMenuActive(), getCurrentAppHandleBounds());
+            mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+        } else {
+            // App header is visible since `mWindowDecorViewHolder` is of type
+            // [AppHeaderViewHolder].
+            ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout(
+                    () -> {
+                        notifyAppChipStateChanged();
+                        return Unit.INSTANCE;
+                    });
+        }
+    }
+
+    private void notifyNoCaptionHandle() {
+        if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+            return;
+        }
+        mWindowDecorCaptionHandleRepository.notifyCaptionChanged(
+                CaptionState.NoCaption.INSTANCE);
+    }
+
+    private Rect getCurrentAppHandleBounds() {
+        return new Rect(
+                mResult.mCaptionX,
+                /* top= */0,
+                mResult.mCaptionX + mResult.mCaptionWidth,
+                mResult.mCaptionHeight);
+    }
+
+    private void notifyAppChipStateChanged() {
+        final Rect appChipPositionInWindow =
+                ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow();
+        final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
+        final Rect appChipGlobalPosition = new Rect(
+                taskBounds.left + appChipPositionInWindow.left,
+                taskBounds.top + appChipPositionInWindow.top,
+                taskBounds.left + appChipPositionInWindow.right,
+                taskBounds.top + appChipPositionInWindow.bottom);
+        final CaptionState captionState = new CaptionState.AppHeader(
+                mTaskInfo,
+                isHandleMenuActive(),
+                appChipGlobalPosition);
+
+        mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+    }
+
+    private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+        return taskInfo.isFreeform() && taskInfo.isResizeable;
+    }
+
     private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
         if (!isDragResizable(mTaskInfo, mContext) || !isMaximizeMenuActive()) {
             return;
@@ -556,7 +637,7 @@
         } else if (mRelayoutParams.mLayoutResId
                 == R.layout.desktop_mode_app_header) {
             loadAppInfoIfNeeded();
-            return new AppHeaderViewHolder(
+            return mAppHeaderViewHolderFactory.create(
                     mResult.mRootView,
                     mOnCaptionTouchListener,
                     mOnCaptionButtonClickListener,
@@ -994,7 +1075,7 @@
                 mAppIconBitmap,
                 mAppName,
                 mSplitScreenController,
-                DesktopModeStatus.canEnterDesktopMode(mContext),
+                canEnterDesktopMode(mContext),
                 supportsMultiInstance,
                 shouldShowManageWindowsButton,
                 getBrowserLink(),
@@ -1027,6 +1108,9 @@
                     return Unit.INSTANCE;
                 }
         );
+        if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+            notifyCaptionStateChanged();
+        }
         mMinimumInstancesFound = false;
     }
 
@@ -1067,7 +1151,10 @@
     }
 
     void closeManageWindowsMenu() {
-        mManageWindowsMenu.close();
+        if (mManageWindowsMenu != null) {
+            mManageWindowsMenu.close();
+        }
+        mManageWindowsMenu = null;
     }
 
     private void updateGenericLink() {
@@ -1089,11 +1176,15 @@
         mWindowDecorViewHolder.onHandleMenuClosed();
         mHandleMenu.close();
         mHandleMenu = null;
+        if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+            notifyCaptionStateChanged();
+        }
     }
 
     @Override
     void releaseViews(WindowContainerTransaction wct) {
         closeHandleMenu();
+        closeManageWindowsMenu();
         closeMaximizeMenu();
         super.releaseViews(wct);
     }
@@ -1257,9 +1348,14 @@
     public void close() {
         closeDragResizeListener();
         closeHandleMenu();
+        closeManageWindowsMenu();
         mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
         disposeResizeVeil();
         disposeStatusBarInputLayer();
+        if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+            notifyNoCaptionHandle();
+        }
+
         super.close();
     }
 
@@ -1367,10 +1463,12 @@
                 @ShellBackgroundThread ShellExecutor bgExecutor,
                 Choreographer choreographer,
                 SyncTransactionQueue syncQueue,
+                AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
                 RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
                 AppToWebGenericLinksParser genericLinksParser,
                 AssistContentRequester assistContentRequester,
                 MultiInstanceHelper multiInstanceHelper,
+                WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
                 WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
             return new DesktopModeWindowDecoration(
                     context,
@@ -1384,10 +1482,12 @@
                     bgExecutor,
                     choreographer,
                     syncQueue,
+                    appHeaderViewHolderFactory,
                     rootTaskDisplayAreaOrganizer,
                     genericLinksParser,
                     assistContentRequester,
                     multiInstanceHelper,
+                    windowDecorCaptionHandleRepository,
                     windowDecorViewHostSupplier);
         }
     }
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 033d695..4a8cabc 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
@@ -22,12 +22,14 @@
 import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.Point
+import android.graphics.Rect
 import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.RippleDrawable
 import android.graphics.drawable.ShapeDrawable
 import android.graphics.drawable.shapes.RoundRectShape
 import android.view.View
 import android.view.View.OnLongClickListener
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
 import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.TextView
@@ -62,7 +64,7 @@
  * finer controls such as a close window button and an "app info" section to pull up additional
  * controls.
  */
-internal class AppHeaderViewHolder(
+class AppHeaderViewHolder(
         rootView: View,
         onCaptionTouchListener: View.OnTouchListener,
         onCaptionButtonClickListener: View.OnClickListener,
@@ -279,6 +281,34 @@
         maximizeButtonView.startHoverAnimation()
     }
 
+    fun runOnAppChipGlobalLayout(runnable: () -> Unit) {
+        if (openMenuButton.isAttachedToWindow) {
+            // App chip is already inflated.
+            runnable()
+            return
+        }
+        // Wait for app chip to be inflated before notifying repository.
+        openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object :
+            OnGlobalLayoutListener {
+            override fun onGlobalLayout() {
+                runnable()
+                openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this)
+            }
+        })
+    }
+
+    fun getAppChipLocationInWindow(): Rect {
+        val appChipBoundsInWindow = IntArray(2)
+        openMenuButton.getLocationInWindow(appChipBoundsInWindow)
+
+        return Rect(
+            /* left = */ appChipBoundsInWindow[0],
+            /* top = */ appChipBoundsInWindow[1],
+            /* right = */ appChipBoundsInWindow[0] + openMenuButton.width,
+            /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height
+        )
+    }
+
     private fun getHeaderStyle(header: Header): HeaderStyle {
         return HeaderStyle(
             background = getHeaderBackground(header),
@@ -529,4 +559,26 @@
         private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65%
         private const val FOCUSED_OPACITY = 255
     }
+
+    class Factory {
+        fun create(
+            rootView: View,
+            onCaptionTouchListener: View.OnTouchListener,
+            onCaptionButtonClickListener: View.OnClickListener,
+            onLongClickListener: OnLongClickListener,
+            onCaptionGenericMotionListener: View.OnGenericMotionListener,
+            appName: CharSequence,
+            appIconBitmap: Bitmap,
+            onMaximizeHoverAnimationFinishedListener: () -> Unit,
+        ): AppHeaderViewHolder = AppHeaderViewHolder(
+            rootView,
+            onCaptionTouchListener,
+            onCaptionButtonClickListener,
+            onLongClickListener,
+            onCaptionGenericMotionListener,
+            appName,
+            appIconBitmap,
+            onMaximizeHoverAnimationFinishedListener,
+        )
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 2341b09..5ea55b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -24,7 +24,7 @@
  * Encapsulates the root [View] of a window decoration and its children to facilitate looking up
  * children (via findViewById) and updating to the latest data from [RunningTaskInfo].
  */
-internal abstract class WindowDecorationViewHolder(rootView: View) {
+abstract class WindowDecorationViewHolder(rootView: View) {
   val context: Context = rootView.context
 
   /**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS
new file mode 100644
index 0000000..622f837
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1168918
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS
new file mode 100644
index 0000000..983e878
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 555586
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS
new file mode 100644
index 0000000..5b05af9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 970984
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
new file mode 100644
index 0000000..e3caf2e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+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
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WindowDecorCaptionHandleRepositoryTest {
+  private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository
+
+  @Before
+  fun setUp() {
+    captionHandleRepository = WindowDecorCaptionHandleRepository()
+  }
+
+  @Test
+  fun initialState_noAction_returnsNoCaption() {
+    // Check the initial value of `captionStateFlow`.
+    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+  }
+
+  @Test
+  fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() {
+    val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
+    val appHandleCaptionState =
+        CaptionState.AppHandle(
+            taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+    captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
+
+    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState)
+  }
+
+  @Test
+  fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() {
+    val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
+    val appHeaderCaptionState =
+        CaptionState.AppHeader(
+            taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+    captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
+
+    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState)
+  }
+
+  @Test
+  fun notifyCaptionChange_toNoCaption_updatesState() {
+    captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption)
+
+    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+  }
+
+  private fun createTaskInfo(
+      deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED,
+      runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME
+  ): RunningTaskInfo =
+      RunningTaskInfo().apply {
+        configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode }
+        topActivityInfo?.apply { packageName = runningTaskPackageName }
+      }
+
+  private companion object {
+    const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+    const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
+  }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS
new file mode 100644
index 0000000..cb12401
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1214056
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS
new file mode 100644
index 0000000..b66cfc3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 785166
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS
new file mode 100644
index 0000000..ad3ca73
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316251
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS
new file mode 100644
index 0000000..ad3ca73
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316251
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS
new file mode 100644
index 0000000..aa019cc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1199235
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS
new file mode 100644
index 0000000..9d926b2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 928697
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS
new file mode 100644
index 0000000..a24088a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316275
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index fec9e3e..aea14b9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -332,6 +332,35 @@
     }
 
     @Test
+    public void testTransitionFilterTaskFragmentToken() {
+        final IBinder taskFragmentToken = new Binder();
+
+        TransitionFilter filter = new TransitionFilter();
+        filter.mRequirements =
+                new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+        filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+        filter.mRequirements[0].mTaskFragmentToken = taskFragmentToken;
+
+        // Transition with the same token should match.
+        final TransitionInfo infoHasTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, taskFragmentToken).build();
+        assertTrue(filter.matches(infoHasTaskFragmentToken));
+
+        // Transition with a different token should not match.
+        final IBinder differentTaskFragmentToken = new Binder();
+        final TransitionInfo infoDifferentTaskFragmentToken =
+                new TransitionInfoBuilder(TRANSIT_OPEN)
+                        .addChange(TRANSIT_OPEN, differentTaskFragmentToken).build();
+        assertFalse(filter.matches(infoDifferentTaskFragmentToken));
+
+        // Transition without a token should not match.
+        final TransitionInfo infoNoTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, createTaskInfo(
+                        1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build();
+        assertFalse(filter.matches(infoNoTaskFragmentToken));
+    }
+
+    @Test
     public void testTransitionFilterMultiRequirement() {
         // filter that requires at-least one opening and one closing app
         TransitionFilter filter = new TransitionFilter();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
index b8939e6f..49ae182 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
@@ -20,8 +20,10 @@
 
 import static org.mockito.Mockito.mock;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.os.IBinder;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -51,21 +53,24 @@
     }
 
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
-            @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo,
-            ComponentName activityComponent) {
+            @TransitionInfo.ChangeFlags int flags,
+            @Nullable ActivityManager.RunningTaskInfo taskInfo,
+            @Nullable ComponentName activityComponent, @Nullable IBinder taskFragmentToken) {
         final TransitionInfo.Change change = new TransitionInfo.Change(
                 taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */));
         change.setMode(mode);
         change.setFlags(flags);
         change.setTaskInfo(taskInfo);
         change.setActivityComponent(activityComponent);
+        change.setTaskFragmentToken(taskFragmentToken);
         return addChange(change);
     }
 
     /** Add a change to the TransitionInfo */
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
             @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
-        return addChange(mode, flags, taskInfo, null /* activityComponent */);
+        return addChange(mode, flags, taskInfo, null /* activityComponent */,
+                null /* taskFragmentToken */);
     }
 
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -76,13 +81,21 @@
     /** Add a change to the TransitionInfo */
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
             ComponentName activityComponent) {
-        return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent);
+        return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent,
+                null /* taskFragmentToken */);
     }
 
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
         return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */);
     }
 
+    /** Add a change with a TaskFragment token to the TransitionInfo */
+    public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+            @Nullable IBinder taskFragmentToken) {
+        return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */,
+                null /* activityComponent */, taskFragmentToken);
+    }
+
     public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
         change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
         mInfo.addChange(change);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS
new file mode 100644
index 0000000..f5ba614
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1267635
+# includes OWNERS from parent directories
\ No newline at end of file
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 85bc7cc..ee2a41c 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
@@ -88,6 +88,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTasksLimiter
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
@@ -98,6 +99,7 @@
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
 import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier
 import java.util.Optional
 import java.util.function.Consumer
@@ -164,6 +166,7 @@
             DesktopModeWindowDecorViewModel.InputMonitorFactory
     @Mock private lateinit var mockShellController: ShellController
     @Mock private lateinit var mockShellExecutor: ShellExecutor
+    @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
     @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
     @Mock private lateinit var mockWindowManager: IWindowManager
@@ -182,6 +185,7 @@
     @Mock private lateinit var mockTaskPositionerFactory:
             DesktopModeWindowDecorViewModel.TaskPositionerFactory
     @Mock private lateinit var mockTaskPositioner: TaskPositioner
+    @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
     @Mock private lateinit var mockWindowDecorViewHostSupplier: WindowDecorViewHostSupplier<*>
     private lateinit var spyContext: TestableContext
 
@@ -236,10 +240,12 @@
                 mockDesktopModeWindowDecorFactory,
                 mockInputMonitorFactory,
                 transactionFactory,
+                mockAppHeaderViewHolderFactory,
                 mockRootTaskDisplayAreaOrganizer,
                 windowDecorByTaskIdSpy,
                 mockInteractionJankMonitor,
                 Optional.of(mockTasksLimiter),
+                mockCaptionHandleRepository,
                 Optional.of(mockActivityOrientationChangeHandler),
                 mockTaskPositionerFactory
         )
@@ -1211,7 +1217,7 @@
         whenever(
             mockDesktopModeWindowDecorFactory.create(
                 any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                any(), any(), any(), any(), any())
+                any(), any(), any(), any(), any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index dff42da..a1867f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -19,9 +19,11 @@
 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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -38,6 +40,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -55,6 +58,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemProperties;
@@ -68,11 +72,13 @@
 import android.view.Choreographer;
 import android.view.Display;
 import android.view.GestureDetector;
+import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.window.WindowContainerTransaction;
 
@@ -94,9 +100,12 @@
 import com.android.wm.shell.common.MultiInstanceHelper;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
 import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost;
 import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
 
@@ -153,6 +162,10 @@
     @Mock
     private SyncTransactionQueue mMockSyncQueue;
     @Mock
+    private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory;
+    @Mock
+    private AppHeaderViewHolder mMockAppHeaderViewHolder;
+    @Mock
     private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
     @Mock
     private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier;
@@ -192,6 +205,8 @@
     private HandleMenuFactory mMockHandleMenuFactory;
     @Mock
     private MultiInstanceHelper mMockMultiInstanceHelper;
+    @Mock
+    private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -245,6 +260,8 @@
         when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay)))
                 .thenReturn(mMockWindowDecorViewHost);
         when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class));
+        when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
+                any())).thenReturn(mMockAppHeaderViewHolder);
     }
 
     @After
@@ -838,6 +855,143 @@
         assertFalse(decoration.isHandleMenuActive());
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+        spyWindowDecor.relayout(taskInfo);
+
+        verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    public void notifyCaptionStateChanged_inFullscreenMode_notifiesAppHandleVisible() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+                CaptionState.class);
+
+        spyWindowDecor.relayout(taskInfo);
+
+        verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+                captionStateArgumentCaptor.capture());
+        assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+                CaptionState.AppHandle.class);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error")
+    public void notifyCaptionStateChanged_inWindowingMode_notifiesAppHeaderVisible() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        when(mMockAppHeaderViewHolder.getAppChipLocationInWindow()).thenReturn(
+                new Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
+        taskInfo.isResizeable = false;
+        ArgumentCaptor<Function0<Unit>> runnableArgumentCaptor = ArgumentCaptor.forClass(
+                Function0.class);
+        ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+                CaptionState.class);
+
+        spyWindowDecor.relayout(taskInfo);
+        verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
+                runnableArgumentCaptor.capture());
+        runnableArgumentCaptor.getValue().invoke();
+
+        verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+                captionStateArgumentCaptor.capture());
+        assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+                CaptionState.AppHeader.class);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    public void notifyCaptionStateChanged_taskNotVisible_notifiesNoCaptionVisible() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ false);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+        ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+                CaptionState.class);
+
+        spyWindowDecor.relayout(taskInfo);
+
+        verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+                captionStateArgumentCaptor.capture());
+        assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+                CaptionState.NoCaption.class);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    public void notifyCaptionStateChanged_captionHandleExpanded_notifiesHandleMenuExpanded() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+                CaptionState.class);
+
+        spyWindowDecor.relayout(taskInfo);
+        createHandleMenu(spyWindowDecor);
+
+        verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+                captionStateArgumentCaptor.capture());
+        assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+                CaptionState.AppHandle.class);
+        assertThat(
+                ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+                        .isHandleMenuExpanded()).isEqualTo(
+                true);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    public void notifyCaptionStateChanged_captionHandleClosed_notifiesHandleMenuClosed() {
+        when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+                .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+        final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+                CaptionState.class);
+
+        spyWindowDecor.relayout(taskInfo);
+        createHandleMenu(spyWindowDecor);
+        spyWindowDecor.closeHandleMenu();
+
+        verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+                captionStateArgumentCaptor.capture());
+        assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+                CaptionState.AppHandle.class);
+        assertThat(
+                ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+                        .isHandleMenuExpanded()).isEqualTo(
+                false);
+
+    }
+
     private void verifyHandleMenuCreated(@Nullable Uri uri) {
         verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
                 any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(),
@@ -906,12 +1060,13 @@
         final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
                 mContext, mMockDisplayController, mMockSplitScreenController,
                 mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
-                mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+                mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory,
+                mMockRootTaskDisplayAreaOrganizer,
                 mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new,
                 mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
                 new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
                 mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory,
-                mMockMultiInstanceHelper);
+                mMockMultiInstanceHelper, mMockCaptionHandleRepository);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
@@ -951,6 +1106,14 @@
                 != 0;
     }
 
+    private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
+        final InsetsState state = new InsetsState();
+        final InsetsSource source = new InsetsSource(/* id= */0, type);
+        source.setVisible(visible);
+        state.addSource(source);
+        return state;
+    }
+
     private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 6342489..306643d 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -451,7 +451,7 @@
     private static final long MAX_SINGLE_LOCATION_TIMEOUT_MS = 30 * 1000;
 
     private static final String CACHE_KEY_LOCATION_ENABLED_PROPERTY =
-            "cache_key.location_enabled";
+            PropertyInvalidatedCache.createSystemCacheKey("location_enabled");
 
     static ILocationManager getService() throws RemoteException {
         try {
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index de85f1e..2804546 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -258,7 +258,7 @@
     /**
      * Mandatory String extra field in {@link #ACTION_TRANSACTION_DETECTED}
      * Indicates the Secure Element on which the transaction occurred.
-     * eSE1...eSEn for Embedded Secure Elements, SIM1...SIMn for UICC, etc.
+     * eSE1...eSEn for Embedded Secure Elements, SIM1...SIMn for UICC/EUICC, etc.
      */
     public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME";
 
@@ -733,7 +733,7 @@
      *
      * @return List<String> containing secure elements on the device which supports
      *                      off host card emulation. eSE for Embedded secure element,
-     *                      SIM for UICC, eSIM for EUICC and so on.
+     *                      SIM for UICC/EUICC and so on.
      * @hide
      */
     public @NonNull List<String> getSupportedOffHostSecureElements() {
@@ -753,11 +753,6 @@
         if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
             offHostSE.add("eSE");
         }
-        if (Flags.enableCardEmulationEuicc()
-                && callServiceReturn(
-                        () -> sCardEmulationService.isEuiccSupported(), false)) {
-            offHostSE.add("eSIM");
-        }
         return offHostSE;
     }
 
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index b28237c..2983875 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -331,8 +331,6 @@
                         mOffHostName = "eSE1";
                     } else if (mOffHostName.equals("SIM")) {
                         mOffHostName = "SIM1";
-                    } else if (Flags.enableCardEmulationEuicc() && mOffHostName.equals("eSIM")) {
-                        mOffHostName = "eSIM1";
                     }
                 }
                 mStaticOffHostName = mOffHostName;
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 83ad32c..4be082c 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -548,13 +548,11 @@
 
         List<String> validSE = adapter.getSupportedOffHostSecureElements();
         if ((offHostSecureElement.startsWith("eSE") && !validSE.contains("eSE"))
-                || (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))
-                || (offHostSecureElement.startsWith("eSIM") && !validSE.contains("eSIM"))) {
+                || (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))) {
             return false;
         }
 
-        if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")
-                && !(Flags.enableCardEmulationEuicc() && offHostSecureElement.startsWith("eSIM"))) {
+        if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")) {
             return false;
         }
 
@@ -562,8 +560,6 @@
             offHostSecureElement = "eSE1";
         } else if (offHostSecureElement.equals("SIM")) {
             offHostSecureElement = "SIM1";
-        } else if (Flags.enableCardEmulationEuicc() && offHostSecureElement.equals("eSIM")) {
-            offHostSecureElement = "eSIM1";
         }
         final String offHostSecureElementV = new String(offHostSecureElement);
         return callServiceReturn(() ->
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 0ae4da5..f64c305 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2016,8 +2016,6 @@
             if (!isValidMediaUri(name, value)) {
                 return false;
             }
-            // Invalidate any relevant cache files
-            cacheFile.delete();
         }
 
         final boolean success;
@@ -2055,6 +2053,11 @@
             return false;
         }
 
+        if (cacheFile != null) {
+            // Invalidate any relevant cache files
+            cacheFile.delete();
+        }
+
         if ((operation == MUTATION_OPERATION_INSERT || operation == MUTATION_OPERATION_UPDATE)
                 && cacheFile != null && value != null) {
             final Uri ringtoneUri = Uri.parse(value);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cd16af7..bd7067b 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -860,6 +860,7 @@
     resource_dirs: [],
     kotlincflags: ["-Xjvm-default=all"],
     optimize: {
+        optimize: false,
         shrink_resources: false,
         optimized_shrink_resources: false,
         proguard_flags_files: ["proguard.flags"],
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index d86890b..437a4b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.biometrics;
 
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_UP;
@@ -1437,4 +1438,38 @@
         // THEN vibrate is used
         verify(mVibrator).performHapticFeedback(any(), eq(UdfpsController.LONG_PRESS));
     }
+
+    @Test
+    public void onAcquiredCalbacks() {
+        runWithAllParams(
+                this::ultrasonicCallbackOnAcquired);
+    }
+
+    public void ultrasonicCallbackOnAcquired(TestParams testParams) throws RemoteException{
+        if (testParams.sensorProps.sensorType
+                == FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC) {
+            reset(mUdfpsView);
+
+            UdfpsController.Callback callbackMock = mock(UdfpsController.Callback.class);
+            mUdfpsController.addCallback(callbackMock);
+
+            // GIVEN UDFPS overlay is showing
+            mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
+                    BiometricRequestConstants.REASON_AUTH_KEYGUARD,
+                    mUdfpsOverlayControllerCallback);
+            mFgExecutor.runAllReady();
+
+            verify(mFingerprintManager).setUdfpsOverlayController(
+                    mUdfpsOverlayControllerCaptor.capture());
+            mUdfpsOverlayControllerCaptor.getValue().onAcquired(0, FINGERPRINT_ACQUIRED_START);
+            mFgExecutor.runAllReady();
+
+            verify(callbackMock).onFingerDown();
+
+            mUdfpsOverlayControllerCaptor.getValue().onAcquired(0, FINGERPRINT_ACQUIRED_GOOD);
+            mFgExecutor.runAllReady();
+
+            verify(callbackMock).onFingerUp();
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
new file mode 100644
index 0000000..fa72d74
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.selection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MutableSelectionStateTest : SysuiTestCase() {
+    private val underTest = MutableSelectionState()
+
+    @Test
+    fun selectTile_isCorrectlySelected() {
+        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+
+        underTest.select(TEST_SPEC)
+        assertThat(underTest.isSelected(TEST_SPEC)).isTrue()
+
+        underTest.unSelect()
+        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+
+        val newSpec = TileSpec.create("newSpec")
+        underTest.select(TEST_SPEC)
+        underTest.select(newSpec)
+        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+        assertThat(underTest.isSelected(newSpec)).isTrue()
+    }
+
+    @Test
+    fun startResize_createsResizingState() {
+        assertThat(underTest.resizingState).isNull()
+
+        // Resizing starts but no tile is selected
+        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+        assertThat(underTest.resizingState).isNull()
+
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC)
+        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+
+        assertThat(underTest.resizingState).isNotNull()
+    }
+
+    @Test
+    fun endResize_clearsResizingState() {
+        val spec = TileSpec.create("testSpec")
+
+        // Resizing starts with a selected tile
+        underTest.select(spec)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        assertThat(underTest.resizingState).isNotNull()
+
+        underTest.onResizingDragEnd()
+        assertThat(underTest.resizingState).isNull()
+    }
+
+    @Test
+    fun unselect_clearsResizingState() {
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        assertThat(underTest.resizingState).isNotNull()
+
+        underTest.unSelect()
+        assertThat(underTest.resizingState).isNull()
+    }
+
+    @Test
+    fun onResizingDrag_updatesResizingState() {
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        assertThat(underTest.resizingState).isNotNull()
+
+        underTest.onResizingDrag(5f)
+        assertThat(underTest.resizingState?.width).isEqualTo(5)
+
+        underTest.onResizingDrag(2f)
+        assertThat(underTest.resizingState?.width).isEqualTo(7)
+
+        underTest.onResizingDrag(-6f)
+        assertThat(underTest.resizingState?.width).isEqualTo(1)
+    }
+
+    @Test
+    fun onResizingDrag_receivesResizeCallback() {
+        var resized = false
+        val onResize: () -> Unit = { resized = !resized }
+
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize)
+        assertThat(underTest.resizingState).isNotNull()
+
+        // Drag under the threshold
+        underTest.onResizingDrag(1f)
+        assertThat(resized).isFalse()
+
+        // Drag over the threshold
+        underTest.onResizingDrag(5f)
+        assertThat(resized).isTrue()
+
+        // Drag back under the threshold
+        underTest.onResizingDrag(-5f)
+        assertThat(resized).isFalse()
+    }
+
+    companion object {
+        private val TEST_SPEC = TileSpec.create("testSpec")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt
new file mode 100644
index 0000000..6e66783
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.selection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizingStateTest : SysuiTestCase() {
+
+    @Test
+    fun drag_updatesStateCorrectly() {
+        var resized = false
+        val underTest =
+            ResizingState(TileWidths(base = 0, min = 0, max = 10)) { resized = !resized }
+
+        assertThat(underTest.width).isEqualTo(0)
+
+        underTest.onDrag(2f)
+        assertThat(underTest.width).isEqualTo(2)
+
+        underTest.onDrag(1f)
+        assertThat(underTest.width).isEqualTo(3)
+        assertThat(resized).isTrue()
+
+        underTest.onDrag(-1f)
+        assertThat(underTest.width).isEqualTo(2)
+        assertThat(resized).isFalse()
+    }
+
+    @Test
+    fun dragOutOfBounds_isClampedCorrectly() {
+        val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) {}
+
+        assertThat(underTest.width).isEqualTo(0)
+
+        underTest.onDrag(100f)
+        assertThat(underTest.width).isEqualTo(10)
+
+        underTest.onDrag(-200f)
+        assertThat(underTest.width).isEqualTo(0)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 3053672..2c8cc1a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,6 +47,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -247,32 +248,35 @@
 
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
-        whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
-        val hmp = createHeadsUpManagerPhone()
-
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val row = mock<ExpandableNotificationRow>()
-        whenever(row.showingPulsing()).thenReturn(false)
-        notifEntry.row = row
-
-        hmp.showNotification(notifEntry)
-        Assert.assertTrue(notifEntry.isSeenInShade)
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
+    fun testShowNotification_removeWhenReorderingAllowedTrue() {
         whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
         val hmp = createHeadsUpManagerPhone()
 
         val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val row = mock<ExpandableNotificationRow>()
-        whenever(row.showingPulsing()).thenReturn(false)
-        notifEntry.row = row
-
         hmp.showNotification(notifEntry)
-        Assert.assertFalse(notifEntry.isSeenInShade)
+        assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(notifEntry)
+        assertThat(notifEntry.isSeenInShade).isTrue();
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(notifEntry)
+        assertThat(notifEntry.isSeenInShade).isFalse();
     }
 
     @Test
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index 84f7a51..6c8db91 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -28,8 +28,4 @@
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">100dp</dimen>
     <dimen name="widget_label_font_size">18sp</dimen>
-
-    <!-- New keyboard shortcut helper -->
-    <dimen name="shortcut_helper_width">704dp</dimen>
-    <dimen name="shortcut_helper_height">1208dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
deleted file mode 100644
index a15532f..0000000
--- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/shortcut_helper_sheet_container"
-    android:layout_gravity="center_horizontal|bottom"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <FrameLayout
-        android:id="@+id/shortcut_helper_sheet"
-        style="@style/ShortcutHelperBottomSheet"
-        android:layout_width="@dimen/shortcut_helper_width"
-        android:layout_height="@dimen/shortcut_helper_height"
-        android:orientation="vertical"
-        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
-
-        <!-- Drag handle for accessibility -->
-        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
-            android:id="@+id/drag_handle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
-
-        <androidx.compose.ui.platform.ComposeView
-            android:id="@+id/shortcut_helper_compose_container"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-    </FrameLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 3efe7a5..2a27b47 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -26,10 +26,6 @@
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
     <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
 
-    <!-- New keyboard shortcut helper -->
-    <dimen name="shortcut_helper_width">864dp</dimen>
-    <dimen name="shortcut_helper_height">728dp</dimen>
-
     <!-- QS-->
     <dimen name="qs_panel_padding_top">16dp</dimen>
     <dimen name="qs_panel_padding">24dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 00846cb..e94248d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1005,10 +1005,6 @@
     <dimen name="ksh_app_item_minimum_height">64dp</dimen>
     <dimen name="ksh_category_separator_margin">16dp</dimen>
 
-    <!-- New keyboard shortcut helper -->
-    <dimen name="shortcut_helper_width">412dp</dimen>
-    <dimen name="shortcut_helper_height">728dp</dimen>
-
     <!-- The size of corner radius of the arrow in the onboarding toast. -->
     <dimen name="recents_onboarding_toast_arrow_corner_radius">2dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ba3822b..a3db776 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3711,6 +3711,12 @@
          [CHAR LIMIT=NONE]
           -->
     <string name="shortcut_helper_key_combinations_or_separator">or</string>
+    <!-- Content description of the drag handle that allows to swipe to dismiss the shortcut helper.
+         The helper is a  component that shows the  user which keyboard shortcuts they can
+         use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_content_description_drag_handle">Drag handle</string>
+
 
     <!-- Keyboard touchpad tutorial scheduler-->
     <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index b10d37e..c95a94e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -34,9 +34,7 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.lightRevealMigration
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
-import com.android.systemui.biometrics.shared.model.toSensorType
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -104,7 +102,6 @@
 
     private var udfpsController: UdfpsController? = null
     private var udfpsRadius: Float = -1f
-    private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN
 
     override fun start() {
         init()
@@ -373,11 +370,8 @@
     private val udfpsControllerCallback =
         object : UdfpsController.Callback {
             override fun onFingerDown() {
-                // only show dwell ripple for device entry non-ultrasonic udfps
-                if (
-                    keyguardUpdateMonitor.isFingerprintDetectionRunning &&
-                        udfpsType != FingerprintSensorType.UDFPS_ULTRASONIC
-                ) {
+                // only show dwell ripple for device entry
+                if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
                     showDwellRipple()
                 }
             }
@@ -403,7 +397,6 @@
             if (it.size > 0) {
                 udfpsController = udfpsControllerProvider.get()
                 udfpsRadius = authController.udfpsRadius
-                udfpsType = it[0].sensorType.toSensorType()
 
                 if (mView.isAttachedToWindow) {
                     udfpsController?.addCallback(udfpsControllerCallback)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 5ffb9ab2..a3904ca 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -19,6 +19,7 @@
 import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT;
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP;
 import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD;
 import static android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING;
@@ -329,6 +330,22 @@
                 int sensorId,
                 @BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo
         ) {
+            if (isUltrasonic()) {
+                if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+                    mFgExecutor.execute(() -> {
+                        for (Callback cb : mCallbacks) {
+                            cb.onFingerDown();
+                        }
+                    });
+                } else {
+                    mFgExecutor.execute(() -> {
+                        for (Callback cb : mCallbacks) {
+                            cb.onFingerUp();
+                        }
+                    });
+                }
+            }
+
             if (BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquiredInfo)) {
                 boolean acquiredGood = acquiredInfo == FINGERPRINT_ACQUIRED_GOOD;
                 mFgExecutor.execute(() -> {
@@ -1024,6 +1041,10 @@
         return mSensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
     }
 
+    private boolean isUltrasonic() {
+        return mSensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+    }
+
     public boolean isFingerDown() {
         return mOnFingerDown;
     }
@@ -1105,8 +1126,10 @@
             }
         }
 
-        for (Callback cb : mCallbacks) {
-            cb.onFingerDown();
+        if (isOptical()) {
+            for (Callback cb : mCallbacks) {
+                cb.onFingerDown();
+            }
         }
     }
 
@@ -1143,8 +1166,10 @@
         if (mOnFingerDown) {
             mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x,
                     y, minor, major, orientation, time, gestureStart, isAod);
-            for (Callback cb : mCallbacks) {
-                cb.onFingerUp();
+            if (isOptical()) {
+                for (Callback cb : mCallbacks) {
+                    cb.onFingerUp();
+                }
             }
         }
         mOnFingerDown = false;
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
index b5e54d5..fdbc18d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -5,7 +5,6 @@
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
 import androidx.compose.ui.platform.ComposeView
-import androidx.core.view.isGone
 import androidx.lifecycle.Lifecycle
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.composable.BouncerContainer
@@ -13,7 +12,6 @@
 import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
 import com.android.systemui.lifecycle.WindowLifecycleState
 import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.lifecycle.setSnapshotBinding
 import com.android.systemui.lifecycle.viewModel
 import kotlinx.coroutines.awaitCancellation
 
@@ -50,7 +48,6 @@
                             setContent { BouncerContainer(viewModelFactory, dialogFactory) }
                         }
                     )
-                    view.setSnapshotBinding { view.isGone = !viewModel.isVisible }
                     awaitCancellation()
                 } finally {
                     view.removeAllViews()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
index c60f932..5a4f8eb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
@@ -16,17 +16,15 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
-import androidx.compose.runtime.getValue
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.lifecycle.ExclusiveActivatable
-import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.kotlin.sample
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
@@ -39,11 +37,6 @@
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
 ) : ExclusiveActivatable() {
 
-    private val hydrator = Hydrator("BouncerContainerViewModel")
-
-    val isVisible: Boolean by
-        hydrator.hydratedStateOf(traceName = "isVisible", source = legacyInteractor.isShowing)
-
     override suspend fun onActivated(): Nothing {
         coroutineScope {
             launch {
@@ -74,8 +67,7 @@
                     legacyInteractor.hide()
                 }
             }
-
-            hydrator.activate()
+            awaitCancellation()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 4bf552e..93cd1cf4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.layout.FlowRowScope
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -71,8 +72,6 @@
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -116,7 +115,6 @@
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.compose.ui.zIndex
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
-import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
@@ -188,10 +186,7 @@
     }
 }
 
-@Composable
-private fun shouldUseSinglePane() =
-    LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
-        LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
+@Composable private fun shouldUseSinglePane() = hasCompactWindowSize()
 
 @Composable
 private fun ShortcutHelperSinglePane(
@@ -425,7 +420,7 @@
     onKeyboardSettingsClicked: () -> Unit,
 ) {
     val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
-    Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) {
+    Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
         TitleBar()
         Spacer(modifier = Modifier.height(12.dp))
         Row(Modifier.fillMaxWidth()) {
@@ -801,6 +796,8 @@
                 style = MaterialTheme.typography.headlineSmall,
             )
         },
+        windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
+        expandedHeight = 64.dp,
     )
 }
 
@@ -835,6 +832,7 @@
         onSearch = {},
         leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
         placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
+        windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
         content = {},
     )
 }
@@ -847,9 +845,7 @@
         shape = RoundedCornerShape(24.dp),
         color = Color.Transparent,
         modifier =
-            Modifier.semantics { role = Role.Button }
-                .fillMaxWidth()
-                .padding(horizontal = 12.dp),
+            Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp),
         interactionSource = interactionSource,
         interactionsConfig =
             InteractionsConfig(
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
new file mode 100644
index 0000000..1f0d696
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.ui.composable
+
+import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+
+/**
+ * returns true if either size of the window is compact. This represents majority of phone windows
+ * portrait
+ */
+@Composable
+fun hasCompactWindowSize() =
+    LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
+        LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 799999a..b9a16c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -18,31 +18,43 @@
 
 import android.content.ActivityNotFoundException
 import android.content.Intent
-import android.graphics.Insets
+import android.content.res.Configuration
 import android.os.Bundle
 import android.provider.Settings
-import android.view.View
-import android.view.WindowInsets
-import androidx.activity.BackEventCompat
 import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Surface
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
-import androidx.core.view.updatePadding
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.flowWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
+import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.dpToPx
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
-import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
 import javax.inject.Inject
 import kotlinx.coroutines.launch
 
@@ -55,52 +67,58 @@
 constructor(private val userTracker: UserTracker, private val viewModel: ShortcutHelperViewModel) :
     ComponentActivity() {
 
-    private val bottomSheetContainer
-        get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
-
-    private val bottomSheet
-        get() = requireViewById<View>(R.id.shortcut_helper_sheet)
-
-    private val bottomSheetBehavior
-        get() = BottomSheetBehavior.from(bottomSheet)
-
     override fun onCreate(savedInstanceState: Bundle?) {
         setupEdgeToEdge()
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_keyboard_shortcut_helper)
-        setUpWidth()
-        expandBottomSheet()
-        setUpInsets()
-        setUpPredictiveBack()
-        setUpSheetDismissListener()
-        setUpDismissOnTouchOutside()
-        setUpComposeView()
+        setContent { Content() }
         observeFinishRequired()
         viewModel.onViewOpened()
     }
 
-    private fun setUpWidth() {
-        // we override this because when maxWidth isn't specified, material imposes a max width
-        // constraint on bottom sheets on larger screens which is smaller than our desired width.
-        bottomSheetBehavior.maxWidth =
-            resources.getDimension(R.dimen.shortcut_helper_width).dpToPx(resources).toInt()
+    @Composable
+    private fun Content() {
+        CompositionLocalProvider(LocalContext provides userTracker.userContext) {
+            PlatformTheme { BottomSheet { finish() } }
+        }
     }
 
-    private fun setUpComposeView() {
-        requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
-            setContent {
-                CompositionLocalProvider(LocalContext provides userTracker.userContext) {
-                    PlatformTheme {
-                        val shortcutsUiState by
-                            viewModel.shortcutsUiState.collectAsStateWithLifecycle()
-                        ShortcutHelper(
-                            shortcutsUiState = shortcutsUiState,
-                            onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
-                            onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
-                        )
-                    }
-                }
-            }
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    private fun BottomSheet(onDismiss: () -> Unit) {
+        ModalBottomSheet(
+            onDismissRequest = { onDismiss() },
+            modifier =
+                Modifier.width(getWidth()).padding(top = getTopPadding()).onKeyEvent {
+                    if (it.key == Key.Escape) {
+                        onDismiss()
+                        true
+                    } else false
+                },
+            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+            dragHandle = { DragHandle() },
+        ) {
+            val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
+            ShortcutHelper(
+                shortcutsUiState = shortcutsUiState,
+                onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
+                onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
+            )
+        }
+    }
+
+    @Composable
+    fun DragHandle() {
+        val dragHandleContentDescription =
+            stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
+        Surface(
+            modifier =
+                Modifier.padding(top = 16.dp, bottom = 6.dp).semantics {
+                    contentDescription = dragHandleContentDescription
+                },
+            color = MaterialTheme.colorScheme.outlineVariant,
+            shape = MaterialTheme.shapes.extraLarge,
+        ) {
+            Box(Modifier.size(width = 32.dp, height = 4.dp))
         }
     }
 
@@ -139,81 +157,27 @@
         window.setDecorFitsSystemWindows(false)
     }
 
-    private fun setUpInsets() {
-        bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
-            val safeDrawingInsets = insets.safeDrawing
-            // Make sure the bottom sheet is not covered by the status bar.
-            bottomSheetBehavior.maxHeight =
-                windowManager.maximumWindowMetrics.bounds.height() - safeDrawingInsets.top
-            // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
-            // cutouts.
-            bottomSheet.updatePadding(
-                left = safeDrawingInsets.left,
-                right = safeDrawingInsets.right,
-                bottom = safeDrawingInsets.bottom,
-            )
-            // The bottom sheet has to be expanded only after setting up insets, otherwise there is
-            // a bug and it will not use full height.
-            expandBottomSheet()
-
-            // Return CONSUMED if you don't want want the window insets to keep passing
-            // down to descendant views.
-            WindowInsets.CONSUMED
-        }
+    @Composable
+    private fun getTopPadding(): Dp {
+        return if (hasCompactWindowSize()) DefaultTopPadding else LargeScreenTopPadding
     }
 
-    private fun expandBottomSheet() {
-        bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
-        bottomSheetBehavior.skipCollapsed = true
-    }
-
-    private fun setUpPredictiveBack() {
-        val onBackPressedCallback =
-            object : OnBackPressedCallback(/* enabled= */ true) {
-                override fun handleOnBackStarted(backEvent: BackEventCompat) {
-                    bottomSheetBehavior.startBackProgress(backEvent)
-                }
-
-                override fun handleOnBackProgressed(backEvent: BackEventCompat) {
-                    bottomSheetBehavior.updateBackProgress(backEvent)
-                }
-
-                override fun handleOnBackPressed() {
-                    bottomSheetBehavior.handleBackInvoked()
-                }
-
-                override fun handleOnBackCancelled() {
-                    bottomSheetBehavior.cancelBackProgress()
-                }
+    @Composable
+    private fun getWidth(): Dp {
+        return if (hasCompactWindowSize()) {
+            DefaultWidth
+        } else
+            when (LocalConfiguration.current.orientation) {
+                Configuration.ORIENTATION_LANDSCAPE -> LargeScreenWidthLandscape
+                else -> LargeScreenWidthPortrait
             }
-        onBackPressedDispatcher.addCallback(
-            owner = this,
-            onBackPressedCallback = onBackPressedCallback,
-        )
     }
 
-    private fun setUpSheetDismissListener() {
-        bottomSheetBehavior.addBottomSheetCallback(
-            object : BottomSheetCallback() {
-                override fun onStateChanged(bottomSheet: View, newState: Int) {
-                    if (newState == STATE_HIDDEN) {
-                        finish()
-                    }
-                }
-
-                override fun onSlide(bottomSheet: View, slideOffset: Float) {}
-            }
-        )
-    }
-
-    private fun setUpDismissOnTouchOutside() {
-        bottomSheetContainer.setOnClickListener { finish() }
+    companion object {
+        private val DefaultTopPadding = 64.dp
+        private val LargeScreenTopPadding = 72.dp
+        private val DefaultWidth = 412.dp
+        private val LargeScreenWidthPortrait = 704.dp
+        private val LargeScreenWidthLandscape = 864.dp
     }
 }
-
-private val WindowInsets.safeDrawing
-    get() =
-        getInsets(WindowInsets.Type.systemBars())
-            .union(getInsets(WindowInsets.Type.displayCutout()))
-
-private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 1f8a24a1..35faa97 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -17,9 +17,10 @@
 package com.android.systemui.qs.panels.ui.compose
 
 import android.content.ClipData
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.draganddrop.dragAndDropSource
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
-import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
 import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
 import androidx.compose.foundation.lazy.grid.LazyGridState
 import androidx.compose.runtime.Composable
@@ -104,11 +105,10 @@
 @Composable
 fun Modifier.dragAndDropTileList(
     gridState: LazyGridState,
-    contentOffset: Offset,
+    contentOffset: () -> Offset,
     dragAndDropState: DragAndDropState,
-    onDrop: () -> Unit,
+    onDrop: (TileSpec) -> Unit,
 ): Modifier {
-    val currentContentOffset by rememberUpdatedState(contentOffset)
     val target =
         remember(dragAndDropState) {
             object : DragAndDropTarget {
@@ -118,7 +118,7 @@
 
                 override fun onMoved(event: DragAndDropEvent) {
                     // Drag offset relative to the list's top left corner
-                    val relativeDragOffset = event.dragOffsetRelativeTo(currentContentOffset)
+                    val relativeDragOffset = event.dragOffsetRelativeTo(contentOffset())
                     val targetItem =
                         gridState.layoutInfo.visibleItemsInfo.firstOrNull { item ->
                             // Check if the drag is on this item
@@ -132,7 +132,7 @@
 
                 override fun onDrop(event: DragAndDropEvent): Boolean {
                     return dragAndDropState.draggedCell?.let {
-                        onDrop()
+                        onDrop(it.tile.tileSpec)
                         dragAndDropState.onDrop()
                         true
                     } ?: false
@@ -158,36 +158,39 @@
     return item.span != 1 && offset.x > itemCenter.x
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun Modifier.dragAndDropTileSource(
     sizedTile: SizedTile<EditTileViewModel>,
     dragAndDropState: DragAndDropState,
-    onTap: (TileSpec) -> Unit,
-    onDoubleTap: (TileSpec) -> Unit = {},
+    onDragStart: () -> Unit,
 ): Modifier {
-    val state by rememberUpdatedState(dragAndDropState)
-    return dragAndDropSource {
-        detectTapGestures(
-            onTap = { onTap(sizedTile.tile.tileSpec) },
-            onDoubleTap = { onDoubleTap(sizedTile.tile.tileSpec) },
-            onLongPress = {
-                state.onStarted(sizedTile)
+    val dragState by rememberUpdatedState(dragAndDropState)
+    @Suppress("DEPRECATION") // b/368361871
+    return dragAndDropSource(
+        block = {
+            detectDragGesturesAfterLongPress(
+                onDrag = { _, _ -> },
+                onDragStart = {
+                    dragState.onStarted(sizedTile)
+                    onDragStart()
 
-                // The tilespec from the ClipData transferred isn't actually needed as we're moving
-                // a tile within the same application. We're using a custom MIME type to limit the
-                // drag event to QS.
-                startTransfer(
-                    DragAndDropTransferData(
-                        ClipData(
-                            QsDragAndDrop.CLIPDATA_LABEL,
-                            arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
-                            ClipData.Item(sizedTile.tile.tileSpec.spec),
+                    // The tilespec from the ClipData transferred isn't actually needed as we're
+                    // moving a tile within the same application. We're using a custom MIME type to
+                    // limit the drag event to QS.
+                    startTransfer(
+                        DragAndDropTransferData(
+                            ClipData(
+                                QsDragAndDrop.CLIPDATA_LABEL,
+                                arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
+                                ClipData.Item(sizedTile.tile.tileSpec.spec),
+                            )
                         )
                     )
-                )
-            },
-        )
-    }
+                },
+            )
+        }
+    )
 }
 
 private object QsDragAndDrop {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 4830ba7..a4f977b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -42,10 +42,8 @@
 }
 
 /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
-class EditTileListState(
-    tiles: List<SizedTile<EditTileViewModel>>,
-    private val columns: Int,
-) : DragAndDropState {
+class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) :
+    DragAndDropState {
     private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null)
     override val draggedCell
         get() = _draggedCell.value
@@ -91,7 +89,8 @@
             regenerateGrid(includeSpacers = true)
             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
         } else {
-            // Add the tile with a temporary row which will get reassigned when regenerating spacers
+            // Add the tile with a temporary row which will get reassigned when
+            // regenerating spacers
             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index aeb6031..8c2fb25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -127,13 +127,14 @@
 }
 
 @Composable
-private fun LargeTileLabels(
+fun LargeTileLabels(
     label: String,
     secondaryLabel: String?,
     colors: TileColors,
+    modifier: Modifier = Modifier,
     accessibilityUiState: AccessibilityUiState? = null,
 ) {
-    Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+    Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
         Text(label, color = colors.label, modifier = Modifier.tileMarquee())
         if (!TextUtils.isEmpty(secondaryLabel)) {
             Text(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index a43b880..0e76e18 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -20,6 +20,9 @@
 
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.foundation.ExperimentalFoundationApi
@@ -31,6 +34,7 @@
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
@@ -38,6 +42,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.grid.GridCells
@@ -56,23 +61,34 @@
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.BiasAlignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
@@ -82,7 +98,9 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.zIndex
 import com.android.compose.modifiers.background
+import com.android.compose.modifiers.height
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
@@ -92,14 +110,23 @@
 import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
 import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileArrangementPadding
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.ToggleTargetSize
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.CurrentTilesGridPadding
+import com.android.systemui.qs.panels.ui.compose.selection.MutableSelectionState
+import com.android.systemui.qs.panels.ui.compose.selection.ResizingHandle
+import com.android.systemui.qs.panels.ui.compose.selection.TileWidths
+import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile
+import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState
+import com.android.systemui.qs.panels.ui.compose.selection.selectableTile
 import com.android.systemui.qs.panels.ui.model.GridCell
 import com.android.systemui.qs.panels.ui.model.SpacerGridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.model.groupAndSort
 import com.android.systemui.res.R
+import kotlinx.coroutines.delay
 
 object TileType
 
@@ -109,14 +136,11 @@
     otherTiles: List<SizedTile<EditTileViewModel>>,
     columns: Int,
     modifier: Modifier,
-    onAddTile: (TileSpec, Int) -> Unit,
     onRemoveTile: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
     onResize: (TileSpec) -> Unit,
 ) {
-    val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
-        onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
-    }
+    val selectionState = rememberSelectionState()
 
     CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
         Column(
@@ -138,7 +162,7 @@
                 }
             }
 
-            CurrentTilesGrid(currentListState, columns, onRemoveTile, onResize, onSetTiles)
+            CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles)
 
             // Hide available tiles when dragging
             AnimatedVisibility(
@@ -153,7 +177,7 @@
                 ) {
                     EditGridHeader { Text(text = "Hold and drag to add tiles.") }
 
-                    AvailableTileGrid(otherTiles, columns, addTileToEnd, currentListState)
+                    AvailableTileGrid(otherTiles, selectionState, columns, currentListState)
                 }
             }
 
@@ -201,61 +225,67 @@
 }
 
 @Composable
-private fun CurrentTilesContainer(content: @Composable () -> Unit) {
-    Box(
-        Modifier.fillMaxWidth()
-            .border(
-                width = 1.dp,
-                color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
-                shape = RoundedCornerShape(48.dp),
-            )
-            .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
-    ) {
-        content()
-    }
-}
-
-@Composable
 private fun CurrentTilesGrid(
     listState: EditTileListState,
+    selectionState: MutableSelectionState,
     columns: Int,
-    onClick: (TileSpec) -> Unit,
     onResize: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
 ) {
     val currentListState by rememberUpdatedState(listState)
-    val tilePadding = CommonTileDefaults.TileArrangementPadding
+    val tileHeight = CommonTileDefaults.TileHeight
+    val totalRows = listState.tiles.lastOrNull()?.row ?: 0
+    val totalHeight by
+        animateDpAsState(
+            gridHeight(totalRows + 1, tileHeight, TileArrangementPadding, CurrentTilesGridPadding),
+            label = "QSEditCurrentTilesGridHeight",
+        )
+    val gridState = rememberLazyGridState()
+    var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+    var droppedSpec by remember { mutableStateOf<TileSpec?>(null) }
 
-    CurrentTilesContainer {
-        val tileHeight = CommonTileDefaults.TileHeight
-        val totalRows = listState.tiles.lastOrNull()?.row ?: 0
-        val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
-        val gridState = rememberLazyGridState()
-        var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+    // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the
+    // selected border and resizing handle, as well as letting the selection animation play.
+    LaunchedEffect(droppedSpec) {
+        droppedSpec?.let {
+            delay(200)
+            selectionState.select(it)
 
-        TileLazyGrid(
-            state = gridState,
-            modifier =
-                Modifier.height(totalHeight)
-                    .dragAndDropTileList(gridState, gridContentOffset, listState) {
-                        onSetTiles(currentListState.tileSpecs())
-                    }
-                    .onGloballyPositioned { coordinates ->
-                        gridContentOffset = coordinates.positionInRoot()
-                    }
-                    .testTag(CURRENT_TILES_GRID_TEST_TAG),
-            columns = GridCells.Fixed(columns),
-        ) {
-            EditTiles(listState.tiles, onClick, listState, onResize = onResize)
+            // Reset droppedSpec in case a tile is dropped twice in a row
+            droppedSpec = null
         }
     }
+
+    TileLazyGrid(
+        state = gridState,
+        columns = GridCells.Fixed(columns),
+        contentPadding = PaddingValues(CurrentTilesGridPadding),
+        modifier =
+            Modifier.fillMaxWidth()
+                .height { totalHeight.roundToPx() }
+                .border(
+                    width = 1.dp,
+                    color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
+                    shape = RoundedCornerShape(48.dp),
+                )
+                .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec ->
+                    onSetTiles(currentListState.tileSpecs())
+                    droppedSpec = spec
+                }
+                .onGloballyPositioned { coordinates ->
+                    gridContentOffset = coordinates.positionInRoot()
+                }
+                .testTag(CURRENT_TILES_GRID_TEST_TAG),
+    ) {
+        EditTiles(listState.tiles, listState, selectionState, onResize)
+    }
 }
 
 @Composable
 private fun AvailableTileGrid(
     tiles: List<SizedTile<EditTileViewModel>>,
+    selectionState: MutableSelectionState,
     columns: Int,
-    onClick: (TileSpec) -> Unit,
     dragAndDropState: DragAndDropState,
 ) {
     // Available tiles aren't visible during drag and drop, so the row isn't needed
@@ -292,7 +322,7 @@
                             cell = tileGridCell,
                             index = index,
                             dragAndDropState = dragAndDropState,
-                            onClick = onClick,
+                            selectionState = selectionState,
                             modifier = Modifier.weight(1f).fillMaxHeight(),
                         )
                     }
@@ -305,8 +335,8 @@
     }
 }
 
-fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
-    return ((tileHeight + padding) * rows) - padding
+fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp {
+    return ((tileHeight + tilePadding) * rows) - tilePadding + gridPadding * 2
 }
 
 private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
@@ -320,9 +350,9 @@
 
 fun LazyGridScope.EditTiles(
     cells: List<GridCell>,
-    onClick: (TileSpec) -> Unit,
     dragAndDropState: DragAndDropState,
-    onResize: (TileSpec) -> Unit = {},
+    selectionState: MutableSelectionState,
+    onResize: (TileSpec) -> Unit,
 ) {
     items(
         count = cells.size,
@@ -347,7 +377,7 @@
                         cell = cell,
                         index = index,
                         dragAndDropState = dragAndDropState,
-                        onClick = onClick,
+                        selectionState = selectionState,
                         onResize = onResize,
                     )
                 }
@@ -361,28 +391,171 @@
     cell: TileGridCell,
     index: Int,
     dragAndDropState: DragAndDropState,
-    onClick: (TileSpec) -> Unit,
-    onResize: (TileSpec) -> Unit = {},
+    selectionState: MutableSelectionState,
+    onResize: (TileSpec) -> Unit,
 ) {
-    val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+    val selected = selectionState.isSelected(cell.tile.tileSpec)
     val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+    val selectionAlpha by
+        animateFloatAsState(
+            targetValue = if (selected) 1f else 0f,
+            label = "QSEditTileSelectionAlpha",
+        )
 
-    EditTile(
-        tileViewModel = cell.tile,
-        iconOnly = cell.isIcon,
-        modifier =
-            Modifier.animateItem()
-                .semantics(mergeDescendants = true) {
-                    onClick(onClickActionName) { false }
-                    this.stateDescription = stateDescription
+    val modifier =
+        Modifier.animateItem()
+            .semantics(mergeDescendants = true) {
+                this.stateDescription = stateDescription
+                contentDescription = cell.tile.label.text
+                customActions =
+                    listOf(
+                        // TODO(b/367748260): Add final accessibility actions
+                        CustomAccessibilityAction("Toggle size") {
+                            onResize(cell.tile.tileSpec)
+                            true
+                        }
+                    )
+            }
+            .height(CommonTileDefaults.TileHeight)
+            .fillMaxWidth()
+
+    val content =
+        @Composable {
+            EditTile(
+                tileViewModel = cell.tile,
+                iconOnly = cell.isIcon,
+                selectionAlpha = { selectionAlpha },
+                modifier =
+                    Modifier.fillMaxSize()
+                        .selectableTile(cell.tile.tileSpec, selectionState)
+                        .dragAndDropTileSource(
+                            SizedTileImpl(cell.tile, cell.width),
+                            dragAndDropState,
+                            selectionState::unSelect,
+                        ),
+            )
+        }
+
+    if (selected) {
+        SelectedTile(
+            tileSpec = cell.tile.tileSpec,
+            isIcon = cell.isIcon,
+            selectionAlpha = { selectionAlpha },
+            selectionState = selectionState,
+            onResize = onResize,
+            modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged
+            content = content,
+        )
+    } else {
+        UnselectedTile(
+            selectionAlpha = { selectionAlpha },
+            selectionState = selectionState,
+            modifier = modifier,
+            content = content,
+        )
+    }
+}
+
+@Composable
+private fun SelectedTile(
+    tileSpec: TileSpec,
+    isIcon: Boolean,
+    selectionAlpha: () -> Float,
+    selectionState: MutableSelectionState,
+    onResize: (TileSpec) -> Unit,
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit,
+) {
+    // Current base, min and max width of this tile
+    var tileWidths: TileWidths? by remember { mutableStateOf(null) }
+
+    // Animated diff between the current width and the resized width of the tile. We can't use
+    // animateContentSize here as the tile is sometimes unbounded.
+    val remainingOffset by
+        animateIntAsState(
+            selectionState.resizingState?.let { tileWidths?.base?.minus(it.width) ?: 0 } ?: 0,
+            label = "QSEditTileWidthOffset",
+        )
+
+    val padding = with(LocalDensity.current) { TileArrangementPadding.roundToPx() }
+    Box(
+        modifier.onSizeChanged {
+            val min = if (isIcon) it.width else (it.width - padding) / 2
+            val max = if (isIcon) (it.width * 2) + padding else it.width
+            tileWidths = TileWidths(it.width, min, max)
+        }
+    ) {
+        val handle =
+            @Composable {
+                ResizingHandle(
+                    enabled = true,
+                    selectionState = selectionState,
+                    transition = selectionAlpha,
+                    tileWidths = { tileWidths },
+                ) {
+                    onResize(tileSpec)
                 }
-                .dragAndDropTileSource(
-                    SizedTileImpl(cell.tile, cell.width),
-                    dragAndDropState,
-                    onClick,
-                    onResize,
-                ),
-    )
+            }
+
+        Layout(contents = listOf(content, handle)) {
+            (contentMeasurables, handleMeasurables),
+            constraints ->
+            // Grab the width from the resizing state if a resize is in progress, otherwise fill the
+            // max width
+            val width =
+                selectionState.resizingState?.width ?: (constraints.maxWidth - remainingOffset)
+            val contentPlaceable =
+                contentMeasurables.first().measure(constraints.copy(maxWidth = width))
+            val handlePlaceable = handleMeasurables.first().measure(constraints)
+
+            // Place the dot vertically centered on the right edge
+            val handleX = contentPlaceable.width - (handlePlaceable.width / 2)
+            val handleY = (contentPlaceable.height / 2) - (handlePlaceable.height / 2)
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                contentPlaceable.place(0, 0)
+                handlePlaceable.place(handleX, handleY)
+            }
+        }
+    }
+}
+
+@Composable
+private fun UnselectedTile(
+    selectionAlpha: () -> Float,
+    selectionState: MutableSelectionState,
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit,
+) {
+    val handle =
+        @Composable {
+            ResizingHandle(
+                enabled = false,
+                selectionState = selectionState,
+                transition = selectionAlpha,
+            )
+        }
+
+    Box(modifier) {
+        Layout(contents = listOf(content, handle)) {
+            (contentMeasurables, handleMeasurables),
+            constraints ->
+            val contentPlaceable =
+                contentMeasurables
+                    .first()
+                    .measure(constraints.copy(maxWidth = constraints.maxWidth))
+            val handlePlaceable = handleMeasurables.first().measure(constraints)
+
+            // Place the dot vertically centered on the right edge
+            val handleX = contentPlaceable.width - (handlePlaceable.width / 2)
+            val handleY = (contentPlaceable.height / 2) - (handlePlaceable.height / 2)
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                contentPlaceable.place(0, 0)
+                handlePlaceable.place(handleX, handleY)
+            }
+        }
+    }
 }
 
 @Composable
@@ -390,8 +563,8 @@
     cell: TileGridCell,
     index: Int,
     dragAndDropState: DragAndDropState,
+    selectionState: MutableSelectionState,
     modifier: Modifier = Modifier,
-    onClick: (TileSpec) -> Unit,
 ) {
     val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
     val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
@@ -403,21 +576,27 @@
         verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top),
         modifier = modifier,
     ) {
-        EditTile(
-            tileViewModel = cell.tile,
-            iconOnly = true,
+        EditTileContainer(
             colors = colors,
             modifier =
-                Modifier.semantics(mergeDescendants = true) {
+                Modifier.fillMaxWidth()
+                    .height(CommonTileDefaults.TileHeight)
+                    .clearSelectionTile(selectionState)
+                    .semantics(mergeDescendants = true) {
                         onClick(onClickActionName) { false }
                         this.stateDescription = stateDescription
                     }
-                    .dragAndDropTileSource(
-                        SizedTileImpl(cell.tile, cell.width),
-                        dragAndDropState,
-                        onTap = onClick,
-                    ),
-        )
+                    .dragAndDropTileSource(SizedTileImpl(cell.tile, cell.width), dragAndDropState) {
+                        selectionState.unSelect()
+                    },
+        ) {
+            // Icon
+            SmallTileContent(
+                icon = cell.tile.icon,
+                color = colors.icon,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
         Box(Modifier.fillMaxSize()) {
             Text(
                 cell.tile.label.text,
@@ -434,7 +613,7 @@
 @Composable
 private fun SpacerGridCell(modifier: Modifier = Modifier) {
     // By default, spacers are invisible and exist purely to catch drag movements
-    Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth().tilePadding())
+    Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth())
 }
 
 @Composable
@@ -443,20 +622,35 @@
     iconOnly: Boolean,
     modifier: Modifier = Modifier,
     colors: TileColors = EditModeTileDefaults.editTileColors(),
+    selectionAlpha: () -> Float = { 1f },
 ) {
-    EditTileContainer(colors = colors, modifier = modifier) {
-        if (iconOnly) {
+    // Animated horizontal alignment from center (0f) to start (-1f)
+    val alignmentValue by
+        animateFloatAsState(
+            targetValue = if (iconOnly) 0f else -1f,
+            label = "QSEditTileContentAlignment",
+        )
+    val alignment by remember {
+        derivedStateOf { BiasAlignment(horizontalBias = alignmentValue, verticalBias = 0f) }
+    }
+
+    EditTileContainer(colors = colors, selectionAlpha = selectionAlpha, modifier = modifier) {
+        // Icon
+        Box(Modifier.size(ToggleTargetSize).align(alignment)) {
             SmallTileContent(
                 icon = tileViewModel.icon,
                 color = colors.icon,
                 modifier = Modifier.align(Alignment.Center),
             )
-        } else {
-            LargeTileContent(
+        }
+
+        // Labels, positioned after the icon
+        AnimatedVisibility(visible = !iconOnly, enter = fadeIn(), exit = fadeOut()) {
+            LargeTileLabels(
                 label = tileViewModel.label.text,
                 secondaryLabel = tileViewModel.appName?.text,
-                icon = tileViewModel.icon,
                 colors = colors,
+                modifier = Modifier.padding(start = ToggleTargetSize + TileArrangementPadding),
             )
         }
     }
@@ -466,27 +660,41 @@
 private fun EditTileContainer(
     colors: TileColors,
     modifier: Modifier = Modifier,
-    content: @Composable BoxScope.() -> Unit,
+    selectionAlpha: () -> Float = { 0f },
+    selectionColor: Color = MaterialTheme.colorScheme.primary,
+    content: @Composable BoxScope.() -> Unit = {},
 ) {
     Box(
-        modifier =
-            modifier
-                .height(CommonTileDefaults.TileHeight)
-                .fillMaxWidth()
-                .drawBehind {
-                    drawRoundRect(
-                        SolidColor(colors.background),
-                        cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
-                    )
-                }
-                .tilePadding(),
-        content = content,
-    )
+        Modifier.wrapContentSize().drawWithContent {
+            drawContent()
+            drawRoundRect(
+                SolidColor(selectionColor),
+                cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+                style = Stroke(EditModeTileDefaults.SelectedBorderWidth.toPx()),
+                alpha = selectionAlpha(),
+            )
+        }
+    ) {
+        Box(
+            modifier =
+                modifier
+                    .drawBehind {
+                        drawRoundRect(
+                            SolidColor(colors.background),
+                            cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+                        )
+                    }
+                    .tilePadding(),
+            content = content,
+        )
+    }
 }
 
 private object EditModeTileDefaults {
     const val PLACEHOLDER_ALPHA = .3f
     val EditGridHeaderHeight = 60.dp
+    val SelectedBorderWidth = 2.dp
+    val CurrentTilesGridPadding = 8.dp
 
     @Composable
     fun editTileColors(): TileColors =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index f96c27d..4946c01 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -98,7 +98,6 @@
             otherTiles = otherTiles,
             columns = columns,
             modifier = modifier,
-            onAddTile = onAddTile,
             onRemoveTile = onRemoveTile,
             onSetTiles = onSetTiles,
             onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 45aad82..afcbed6d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.layout.Arrangement.spacedBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -82,6 +83,7 @@
     columns: GridCells,
     modifier: Modifier = Modifier,
     state: LazyGridState = rememberLazyGridState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
     content: LazyGridScope.() -> Unit,
 ) {
     LazyVerticalGrid(
@@ -89,6 +91,7 @@
         columns = columns,
         verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
         horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+        contentPadding = contentPadding,
         modifier = modifier,
         content = content,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
new file mode 100644
index 0000000..2ea32e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.selection
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Creates the state of the current selected tile that is remembered across compositions. */
+@Composable
+fun rememberSelectionState(): MutableSelectionState {
+    return remember { MutableSelectionState() }
+}
+
+/** Holds the state of the current selection. */
+class MutableSelectionState {
+    private var _selectedTile = mutableStateOf<TileSpec?>(null)
+    private var _resizingState = mutableStateOf<ResizingState?>(null)
+
+    /** The [ResizingState] of the selected tile is currently being resized, null if not. */
+    val resizingState by _resizingState
+
+    fun isSelected(tileSpec: TileSpec): Boolean {
+        return _selectedTile.value?.let { it == tileSpec } ?: false
+    }
+
+    fun select(tileSpec: TileSpec) {
+        _selectedTile.value = tileSpec
+    }
+
+    fun unSelect() {
+        _selectedTile.value = null
+        onResizingDragEnd()
+    }
+
+    fun onResizingDrag(offset: Float) {
+        _resizingState.value?.onDrag(offset)
+    }
+
+    fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) {
+        if (_selectedTile.value == null) return
+
+        _resizingState.value = ResizingState(tileWidths, onResize)
+    }
+
+    fun onResizingDragEnd() {
+        _resizingState.value = null
+    }
+}
+
+/**
+ * Listens for click events to select/unselect the given [TileSpec]. Use this on current tiles as
+ * they can be selected.
+ */
+@Composable
+fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier {
+    return pointerInput(Unit) {
+        detectTapGestures(
+            onTap = {
+                if (selectionState.isSelected(tileSpec)) {
+                    selectionState.unSelect()
+                } else {
+                    selectionState.select(tileSpec)
+                }
+            }
+        )
+    }
+}
+
+/**
+ * Listens for click events to unselect any tile. Use this on available tiles as they can't be
+ * selected.
+ */
+@Composable
+fun Modifier.clearSelectionTile(selectionState: MutableSelectionState): Modifier {
+    return pointerInput(Unit) { detectTapGestures(onTap = { selectionState.unSelect() }) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
new file mode 100644
index 0000000..a084bc2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.selection
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD
+
+class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) {
+    // Total drag offset of this resize operation
+    private var totalOffset = 0f
+
+    /** Width in pixels of the resizing tile. */
+    var width by mutableIntStateOf(widths.base)
+
+    // Whether the tile is currently over the threshold and should be a large tile
+    private var passedThreshold: Boolean = passedThreshold(calculateProgression(width))
+
+    fun onDrag(offset: Float) {
+        totalOffset += offset
+        width = (widths.base + totalOffset).toInt().coerceIn(widths.min, widths.max)
+
+        passedThreshold(calculateProgression(width)).let {
+            // Resize if we went over the threshold
+            if (passedThreshold != it) {
+                passedThreshold = it
+                onResize()
+            }
+        }
+    }
+
+    private fun passedThreshold(progression: Float): Boolean {
+        return progression >= RESIZING_THRESHOLD
+    }
+
+    /** The progression of the resizing tile between an icon tile (0f) and a large tile (1f) */
+    private fun calculateProgression(width: Int): Float {
+        return ((width - widths.min) / (widths.max - widths.min).toFloat()).coerceIn(0f, 1f)
+    }
+}
+
+/** Holds the width of a tile as well as its min and max widths */
+data class TileWidths(val base: Int, val min: Int, val max: Int) {
+    init {
+        check(max > min) { "The max width needs to be larger than the min width." }
+    }
+}
+
+private object ResizingDefaults {
+    const val RESIZING_THRESHOLD = .25f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
new file mode 100644
index 0000000..e3acf38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.selection
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize
+
+/**
+ * Dot handling resizing drag events. Use this on the selected tile to resize it
+ *
+ * @param enabled whether resizing drag events should be handled
+ * @param selectionState the [MutableSelectionState] on the grid
+ * @param transition the animated value for the dot, used for its alpha and scale
+ * @param tileWidths the [TileWidths] of the selected tile
+ * @param onResize the callback when the drag passes the resizing threshold
+ */
+@Composable
+fun ResizingHandle(
+    enabled: Boolean,
+    selectionState: MutableSelectionState,
+    transition: () -> Float,
+    tileWidths: () -> TileWidths? = { null },
+    onResize: () -> Unit = {},
+) {
+    if (enabled) {
+        // Manually creating the touch target around the resizing dot to ensure that the next tile
+        // does
+        // not receive the touch input accidentally.
+        val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current
+        Box(
+            Modifier.size(minTouchTargetSize).pointerInput(Unit) {
+                detectHorizontalDragGestures(
+                    onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
+                    onDragStart = {
+                        tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) }
+                    },
+                    onDragEnd = selectionState::onResizingDragEnd,
+                    onDragCancel = selectionState::onResizingDragEnd,
+                )
+            }
+        ) {
+            ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center))
+        }
+    } else {
+        ResizingDot(transition = transition)
+    }
+}
+
+@Composable
+private fun ResizingDot(
+    transition: () -> Float,
+    modifier: Modifier = Modifier,
+    color: Color = MaterialTheme.colorScheme.primary,
+) {
+    Canvas(modifier = modifier.size(ResizingDotSize)) {
+        val v = transition()
+        drawCircle(color = color, radius = (ResizingDotSize / 2).toPx() * v, alpha = v)
+    }
+}
+
+private object SelectionDefaults {
+    val ResizingDotSize = 16.dp
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 1c223db..bf88807 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -30,6 +30,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.core.view.ViewKt;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
 import com.android.keyguard.KeyguardUnfoldTransition;
@@ -38,6 +40,7 @@
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
 import com.android.systemui.bouncer.ui.binder.BouncerViewBinder;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.SysUISingleton;
@@ -51,10 +54,12 @@
 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.Edge;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.shared.model.Scenes;
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -111,6 +116,7 @@
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final QuickSettingsController mQuickSettingsController;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private final GlanceableHubContainerController
             mGlanceableHubContainerController;
     private GestureDetector mPulsingWakeupGestureHandler;
@@ -140,6 +146,7 @@
     private final PanelExpansionInteractor mPanelExpansionInteractor;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
 
+    private ViewGroup mBouncerParentView;
     /**
      * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been
      * intercepted and all future touch events for the gesture should be processed by this view.
@@ -217,6 +224,7 @@
         mPulsingGestureListener = pulsingGestureListener;
         mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
         mNotificationInsetsController = notificationInsetsController;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mGlanceableHubContainerController = glanceableHubContainerController;
         mFeatureFlagsClassic = featureFlagsClassic;
         mSysUIKeyEventHandler = sysUIKeyEventHandler;
@@ -227,7 +235,7 @@
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
         mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView);
-        bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container));
+        bindBouncer(bouncerViewBinder);
 
         collectFlow(mView, keyguardTransitionInteractor.transition(
                 Edge.create(LOCKSCREEN, DREAMING)),
@@ -256,6 +264,35 @@
         dumpManager.registerDumpable(this);
     }
 
+    private void bindBouncer(BouncerViewBinder bouncerViewBinder) {
+        if (ComposeBouncerFlags.INSTANCE.isOnlyComposeBouncerEnabled()) {
+            collectFlow(mView, mKeyguardTransitionInteractor.isFinishedIn(Scenes.Gone,
+                    KeyguardState.GONE), this::removeBouncerParentView);
+            collectFlow(mView, mKeyguardTransitionInteractor.transition(
+                            new Edge.StateToState(KeyguardState.GONE, null)),
+                    this::handleGoneToAnyOtherStateTransition);
+            collectFlow(mView, mPrimaryBouncerInteractor.isShowing(),
+                    (showing) -> ViewKt.setVisible(mBouncerParentView, showing));
+        }
+        mBouncerParentView = mView.findViewById(R.id.keyguard_bouncer_container);
+        bouncerViewBinder.bind(mBouncerParentView);
+    }
+
+    private void handleGoneToAnyOtherStateTransition(TransitionStep transitionStep) {
+        if (transitionStep.getTransitionState() == TransitionState.STARTED) {
+            if (mView.indexOfChild(mBouncerParentView) != -1) {
+                mView.removeView(mBouncerParentView);
+            }
+            mView.addView(mBouncerParentView);
+        }
+    }
+
+    private void removeBouncerParentView(boolean isFinishedInGoneState) {
+        if (isFinishedInGoneState) {
+            mView.removeView(mBouncerParentView);
+        }
+    }
+
     /**
      * @return Location where to place the KeyguardMessageArea
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
deleted file mode 100644
index 4c0c461..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
+++ /dev/null
@@ -1,61 +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.chips.shared
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the status bar ron chips flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object StatusBarRonChips {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.statusBarRonChips()
-
-    /**
-     * Called to ensure code is only run when the flag is enabled. This protects users from the
-     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
-     * build to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun isUnexpectedlyInLegacyMode() =
-        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-    /**
-     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
-     * the flag is not enabled to ensure that the refactor author catches issues in testing.
-     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
-     */
-    @JvmStatic
-    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
-
-    /**
-     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
-     * the flag is enabled to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 02a29e2..9b96931 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,7 +103,8 @@
     private boolean mTrackingHeadsUp;
     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
-    private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+    @VisibleForTesting
+    public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
     private boolean mIsExpanded;
     private int mStatusBarState;
@@ -417,7 +418,7 @@
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
-                removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
+                removeEntry(entry.getKey(), "allowReorder");
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -617,11 +618,8 @@
             super.setEntry(entry, removeRunnable);
 
             if (NotificationThrottleHun.isEnabled()) {
-                if (!mVisualStabilityProvider.isReorderingAllowed()
-                        // We don't want to allow reordering while pulsing, but headsup need to
-                        // time out anyway
-                        && !entry.showingPulsing()) {
-                    mEntriesToRemoveWhenReorderingAllowed.add(entry);
+                mEntriesToRemoveWhenReorderingAllowed.add(entry);
+                if (!mVisualStabilityProvider.isReorderingAllowed()) {
                     entry.setSeenInShade(true);
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 659cee3..6584556 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -55,7 +55,7 @@
 import com.android.systemui.statusbar.OperatorNameView;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.chips.shared.StatusBarRonChips;
+import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 5ba5c06..1127f6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,6 +46,8 @@
 
     private val tag = "AvalancheController"
     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
+    var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
+
     var enableAtRuntime = true
         set(value) {
             if (!value) {
@@ -116,32 +118,43 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
+            headsUpManagerLogger.logAvalancheUpdate(
+                caller, isEnabled, key,
+                "Runnable NULL, stop. ${getStateStr()}"
+            )
             return
         }
         if (!isEnabled) {
-            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
-                    "NOT ENABLED, run runnable")
+            headsUpManagerLogger.logAvalancheUpdate(
+                caller, isEnabled, key,
+                "NOT ENABLED, run runnable. ${getStateStr()}"
+            )
             runnable.run()
             return
         }
         if (entry == null) {
-            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
+            headsUpManagerLogger.logAvalancheUpdate(
+                caller, isEnabled, key,
+                "Entry NULL, stop. ${getStateStr()}"
+            )
             return
         }
         if (debug) {
             debugRunnableLabelMap[runnable] = caller
         }
-        var outcome = ""
+        var stateAfter = ""
         if (isShowing(entry)) {
-            outcome = "update showing"
             runnable.run()
+            stateAfter = "update showing"
+
         } else if (entry in nextMap) {
-            outcome = "update next"
             nextMap[entry]?.add(runnable)
+            stateAfter = "update next"
+
         } else if (headsUpEntryShowing == null) {
-            outcome = "show now"
             showNow(entry, arrayListOf(runnable))
+            stateAfter = "show now"
+
         } else {
             // Clean up invalid state when entry is in list but not map and vice versa
             if (entry in nextMap) nextMap.remove(entry)
@@ -162,8 +175,8 @@
                 )
             }
         }
-        outcome += getStateStr()
-        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
+        stateAfter += getStateStr()
+        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
     }
 
     @VisibleForTesting
@@ -181,32 +194,40 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
+            headsUpManagerLogger.logAvalancheDelete(
+                caller, isEnabled, key,
+                "Runnable NULL, stop. ${getStateStr()}"
+            )
             return
         }
         if (!isEnabled) {
-            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
-                    "NOT ENABLED, run runnable")
             runnable.run()
+            headsUpManagerLogger.logAvalancheDelete(
+                caller, isEnabled = false, key,
+                "NOT ENABLED, run runnable. ${getStateStr()}"
+            )
             return
         }
         if (entry == null) {
-            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
-                    "Entry NULL, run runnable")
             runnable.run()
+            headsUpManagerLogger.logAvalancheDelete(
+                caller, isEnabled = true, key,
+                "Entry NULL, run runnable. ${getStateStr()}"
+            )
             return
         }
-        var outcome = ""
+        var stateAfter: String
         if (entry in nextMap) {
-            outcome = "remove from next"
             if (entry in nextMap) nextMap.remove(entry)
             if (entry in nextList) nextList.remove(entry)
             uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
+            stateAfter = "remove from next. ${getStateStr()}"
+
         } else if (entry in debugDropSet) {
-            outcome = "remove from dropset"
             debugDropSet.remove(entry)
+            stateAfter = "remove from dropset. ${getStateStr()}"
+
         } else if (isShowing(entry)) {
-            outcome = "remove showing"
             previousHunKey = getKey(headsUpEntryShowing)
             // Show the next HUN before removing this one, so that we don't tell listeners
             // onHeadsUpPinnedModeChanged, which causes
@@ -214,11 +235,13 @@
             // HUN is animating out, resulting in a flicker.
             showNext()
             runnable.run()
+            stateAfter = "remove showing. ${getStateStr()}"
+
         } else {
-            outcome = "run runnable for untracked shown"
             runnable.run()
+            stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
         }
-        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
+        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
     }
 
     /**
@@ -400,12 +423,14 @@
     }
 
     private fun getStateStr(): String {
-        return "\navalanche state:" +
+        return "\nAvalancheController:" +
                 "\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
                 "\n\tprevious: [$previousHunKey]" +
                 "\n\tnext list: $nextListStr" +
                 "\n\tnext map: $nextMapStr" +
-                "\n\tdropped: $dropSetStr"
+                "\n\tdropped: $dropSetStr" +
+                "\nBHUM.mHeadsUpEntryMap: " +
+                baseEntryMapStr()
     }
 
     private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index f37393a..30524a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,6 +116,7 @@
         mAccessibilityMgr = accessibilityManagerWrapper;
         mUiEventLogger = uiEventLogger;
         mAvalancheController = avalancheController;
+        mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
         Resources resources = context.getResources();
         mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
                 ? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -589,6 +590,18 @@
         dumpInternal(pw, args);
     }
 
+    private String getEntryMapStr() {
+        if (mHeadsUpEntryMap.isEmpty()) {
+            return "EMPTY";
+        }
+        StringBuilder entryMapStr = new StringBuilder();
+        for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
+            entryMapStr.append("\n\t").append(
+                    entry.mEntry == null ? "null" : entry.mEntry.getKey());
+        }
+        return entryMapStr.toString();
+    }
+
     protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -992,7 +1005,6 @@
          * Clear any pending removal runnables.
          */
         public void cancelAutoRemovalCallbacks(@Nullable String reason) {
-            mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
             Runnable runnable = () -> {
                 final boolean removed = cancelAutoRemovalCallbackInternal();
 
@@ -1001,6 +1013,7 @@
                 }
             };
             if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
+                mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
                 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
             } else {
                 // Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 600270c..41112cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        outcome: String
+        stateAfter: String
     ) {
         buffer.log(
             TAG,
@@ -60,7 +60,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = outcome
+                str3 = stateAfter
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        outcome: String
+        stateAfter: String
     ) {
         buffer.log(
             TAG,
@@ -79,7 +79,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = outcome
+                str3 = stateAfter
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "request: cancel auto remove of $str1 reason: $str2" }
+            { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
         )
     }
 
@@ -148,7 +148,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "cancel auto remove of $str1 reason: $str2" }
+            { "$str2 => cancel auto remove: $str1" }
         )
     }
 
@@ -161,7 +161,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
+            { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -174,7 +174,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
+            { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -216,12 +216,12 @@
                 str1 = logKey(key)
                 str2 = reason
             },
-            { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
+            { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
         )
     }
 
     fun logNotificationActuallyRemoved(entry: NotificationEntry) {
-        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
+        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
     }
 
     fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
+            { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -246,7 +246,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
+            { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -281,7 +281,7 @@
                 bool1 = isPinned
                 str2 = reason
             },
-            { "$str2 => set entry pinned $str1 pinned: $bool1" }
+            { "$str2 => setEntryPinned[$bool1]: $str1" }
         )
     }
 
@@ -290,7 +290,7 @@
             TAG,
             INFO,
             { bool1 = hasPinnedNotification },
-            { "has pinned notification changed to $bool1" }
+            { "hasPinnedNotification[$bool1]" }
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index bbfa32b..32a4f12 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -117,7 +117,14 @@
             int displayId) {
         Runnable showToastRunnable = () -> {
             UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
-            Context context = mContext.createContextAsUser(userHandle, 0);
+            Context context;
+            try {
+                context = mContext.createContextAsUser(userHandle, 0);
+            } catch (IllegalStateException e) {
+                // b/366533044 : Own package not found for systemui
+                Log.e(TAG, "Cannot create toast because cannot create context", e);
+                return;
+            }
 
             DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class);
             Display display = mDisplayManager.getDisplay(displayId);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 7385b82..e764015 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -22,6 +22,7 @@
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
 import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
@@ -449,7 +450,8 @@
             @Override
             public void onEntryRemoved(NotificationEntry entry,
                     @NotifCollection.CancellationReason int reason) {
-                if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) {
+                if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL
+                        || reason == REASON_PACKAGE_BANNED) {
                     BubblesManager.this.onEntryRemoved(entry);
                 }
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index bbff539..6dc4b10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -18,10 +18,6 @@
 
 import android.graphics.Point
 import android.hardware.biometrics.BiometricSourceType
-import android.hardware.biometrics.ComponentInfoInternal
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
-import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.testing.TestableLooper.RunWithLooper
 import android.util.DisplayMetrics
@@ -47,7 +43,6 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.mockito.any
-import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.After
 import org.junit.Assert.assertFalse
@@ -67,6 +62,8 @@
 import org.mockito.MockitoAnnotations
 import org.mockito.MockitoSession
 import org.mockito.quality.Strictness
+import javax.inject.Provider
+
 
 @ExperimentalCoroutinesApi
 @SmallTest
@@ -82,28 +79,35 @@
     @Mock private lateinit var authController: AuthController
     @Mock private lateinit var authRippleInteractor: AuthRippleInteractor
     @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
-    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
-    @Mock private lateinit var biometricUnlockController: BiometricUnlockController
-    @Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController>
-    @Mock private lateinit var udfpsController: UdfpsController
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
-    @Mock private lateinit var lightRevealScrim: LightRevealScrim
-    @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
+    @Mock
+    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock
+    private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+    @Mock
+    private lateinit var biometricUnlockController: BiometricUnlockController
+    @Mock
+    private lateinit var udfpsControllerProvider: Provider<UdfpsController>
+    @Mock
+    private lateinit var udfpsController: UdfpsController
+    @Mock
+    private lateinit var statusBarStateController: StatusBarStateController
+    @Mock
+    private lateinit var lightRevealScrim: LightRevealScrim
+    @Mock
+    private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
 
     private val facePropertyRepository = FakeFacePropertyRepository()
     private val displayMetrics = DisplayMetrics()
 
     @Captor
     private lateinit var biometricUnlockListener:
-        ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
+            ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
 
     @Before
     fun setUp() {
         mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         MockitoAnnotations.initMocks(this)
-        staticMockSession =
-            mockitoSession()
+        staticMockSession = mockitoSession()
                 .mockStatic(RotationUtils::class.java)
                 .strictness(Strictness.LENIENT)
                 .startMocking()
@@ -112,26 +116,25 @@
         `when`(authController.udfpsProps).thenReturn(listOf(fpSensorProp))
         `when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
 
-        controller =
-            AuthRippleController(
-                context,
-                authController,
-                configurationController,
-                keyguardUpdateMonitor,
-                keyguardStateController,
-                wakefulnessLifecycle,
-                commandRegistry,
-                notificationShadeWindowController,
-                udfpsControllerProvider,
-                statusBarStateController,
-                displayMetrics,
-                KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
-                biometricUnlockController,
-                lightRevealScrim,
-                authRippleInteractor,
-                facePropertyRepository,
-                rippleView,
-            )
+        controller = AuthRippleController(
+            context,
+            authController,
+            configurationController,
+            keyguardUpdateMonitor,
+            keyguardStateController,
+            wakefulnessLifecycle,
+            commandRegistry,
+            notificationShadeWindowController,
+            udfpsControllerProvider,
+            statusBarStateController,
+            displayMetrics,
+            KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
+            biometricUnlockController,
+            lightRevealScrim,
+            authRippleInteractor,
+            facePropertyRepository,
+            rippleView,
+        )
         controller.init()
     }
 
@@ -147,18 +150,13 @@
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
         `when`(keyguardStateController.isShowing).thenReturn(true)
-        `when`(
-                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    eq(BiometricSourceType.FINGERPRINT)
-                )
-            )
-            .thenReturn(true)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
 
         // WHEN fingerprint authenticated
         verify(biometricUnlockController).addListener(biometricUnlockListener.capture())
-        biometricUnlockListener.value.onBiometricUnlockedWithKeyguardDismissal(
-            BiometricSourceType.FINGERPRINT
-        )
+        biometricUnlockListener.value
+                .onBiometricUnlockedWithKeyguardDismissal(BiometricSourceType.FINGERPRINT)
 
         // THEN update sensor location and show ripple
         verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f)
@@ -171,12 +169,8 @@
         val fpsLocation = Point(5, 5)
         `when`(authController.udfpsLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
-        `when`(
-                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    eq(BiometricSourceType.FINGERPRINT)
-                )
-            )
-            .thenReturn(true)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
 
         // WHEN keyguard is NOT showing & fingerprint authenticated
         `when`(keyguardStateController.isShowing).thenReturn(false)
@@ -185,8 +179,7 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */
-        )
+            false /* isStrongBiometric */)
 
         // THEN no ripple
         verify(rippleView, never()).startUnlockedRipple(any())
@@ -201,19 +194,14 @@
         `when`(keyguardStateController.isShowing).thenReturn(true)
 
         // WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated
-        `when`(
-                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    eq(BiometricSourceType.FINGERPRINT)
-                )
-            )
-            .thenReturn(false)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                eq(BiometricSourceType.FINGERPRINT))).thenReturn(false)
         val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
         verify(keyguardUpdateMonitor).registerCallback(captor.capture())
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */
-        )
+            false /* isStrongBiometric */)
 
         // THEN no ripple
         verify(rippleView, never()).startUnlockedRipple(any())
@@ -230,8 +218,7 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FACE /* type */,
-            false /* isStrongBiometric */
-        )
+            false /* isStrongBiometric */)
         verify(rippleView, never()).startUnlockedRipple(any())
     }
 
@@ -246,17 +233,18 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */
-        )
+            false /* isStrongBiometric */)
         verify(rippleView, never()).startUnlockedRipple(any())
     }
 
     @Test
     fun registersAndDeregisters() {
         controller.onViewAttached()
-        val captor = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java)
+        val captor = ArgumentCaptor
+            .forClass(KeyguardStateController.Callback::class.java)
         verify(keyguardStateController).addCallback(captor.capture())
-        val captor2 = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
+        val captor2 = ArgumentCaptor
+            .forClass(WakefulnessLifecycle.Observer::class.java)
         verify(wakefulnessLifecycle).addObserver(captor2.capture())
         controller.onViewDetached()
         verify(keyguardStateController).removeCallback(any())
@@ -271,25 +259,17 @@
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
         `when`(keyguardStateController.isShowing).thenReturn(true)
-        `when`(
-                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    BiometricSourceType.FINGERPRINT
-                )
-            )
-            .thenReturn(true)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                BiometricSourceType.FINGERPRINT)).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
         controller.showUnlockRipple(BiometricSourceType.FINGERPRINT)
-        assertTrue(
-            "reveal didn't start on keyguardFadingAway",
-            controller.startLightRevealScrimOnKeyguardFadingAway
-        )
+        assertTrue("reveal didn't start on keyguardFadingAway",
+            controller.startLightRevealScrimOnKeyguardFadingAway)
         `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
         controller.onKeyguardFadingAwayChanged()
-        assertFalse(
-            "reveal triggers multiple times",
-            controller.startLightRevealScrimOnKeyguardFadingAway
-        )
+        assertFalse("reveal triggers multiple times",
+            controller.startLightRevealScrimOnKeyguardFadingAway)
     }
 
     @Test
@@ -302,27 +282,23 @@
         `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
         `when`(authController.isUdfpsFingerDown).thenReturn(true)
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(eq(BiometricSourceType.FACE)))
-            .thenReturn(true)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                eq(BiometricSourceType.FACE))).thenReturn(true)
 
         controller.showUnlockRipple(BiometricSourceType.FACE)
-        assertTrue(
-            "reveal didn't start on keyguardFadingAway",
-            controller.startLightRevealScrimOnKeyguardFadingAway
-        )
+        assertTrue("reveal didn't start on keyguardFadingAway",
+                controller.startLightRevealScrimOnKeyguardFadingAway)
         `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
         controller.onKeyguardFadingAwayChanged()
-        assertFalse(
-            "reveal triggers multiple times",
-            controller.startLightRevealScrimOnKeyguardFadingAway
-        )
+        assertFalse("reveal triggers multiple times",
+                controller.startLightRevealScrimOnKeyguardFadingAway)
     }
 
     @Test
     fun testUpdateRippleColor() {
         controller.onViewAttached()
-        val captor =
-            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+        val captor = ArgumentCaptor
+            .forClass(ConfigurationController.ConfigurationListener::class.java)
         verify(configurationController).addCallback(captor.capture())
 
         reset(rippleView)
@@ -357,40 +333,6 @@
     }
 
     @Test
-    fun testUltrasonicUdfps_onFingerDown_runningForDeviceEntry_doNotShowDwellRipple() {
-        // GIVEN UDFPS is ultrasonic
-        `when`(authController.udfpsProps)
-            .thenReturn(
-                listOf(
-                    FingerprintSensorPropertiesInternal(
-                        0 /* sensorId */,
-                        SensorProperties.STRENGTH_STRONG,
-                        5 /* maxEnrollmentsPerUser */,
-                        listOf<ComponentInfoInternal>(),
-                        FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
-                        false /* halControlsIllumination */,
-                        true /* resetLockoutRequiresHardwareAuthToken */,
-                        listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
-                    )
-                )
-            )
-
-        // GIVEN fingerprint detection is running on keyguard
-        `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true)
-
-        // GIVEN view is already attached
-        controller.onViewAttached()
-        val captor = ArgumentCaptor.forClass(UdfpsController.Callback::class.java)
-        verify(udfpsController).addCallback(captor.capture())
-
-        // WHEN finger is down
-        captor.value.onFingerDown()
-
-        // THEN never show dwell ripple
-        verify(rippleView, never()).startDwellRipple(false)
-    }
-
-    @Test
     fun testUdfps_onFingerDown_notDeviceEntry_doesNotShowDwellRipple() {
         // GIVEN fingerprint detection is NOT running on keyguard
         `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 755adc6..6423d25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -64,7 +64,6 @@
             otherTiles = listOf(),
             columns = 4,
             modifier = Modifier.fillMaxSize(),
-            onAddTile = { _, _ -> },
             onRemoveTile = {},
             onSetTiles = onSetTiles,
             onResize = {},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
new file mode 100644
index 0000000..682ed92
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel
+import androidx.compose.ui.text.AnnotatedString
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizingTest : SysuiTestCase() {
+    @get:Rule val composeRule = createComposeRule()
+
+    @Composable
+    private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) {
+        DefaultEditTileGrid(
+            currentListState = listState,
+            otherTiles = listOf(),
+            columns = 4,
+            modifier = Modifier.fillMaxSize(),
+            onRemoveTile = {},
+            onSetTiles = {},
+            onResize = onResize,
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun resizedIcon_shouldBeLarge() {
+        var tiles by mutableStateOf(TestEditTiles)
+        val listState = EditTileListState(tiles, 4)
+        composeRule.setContent {
+            EditTileGridUnderTest(listState) { spec ->
+                tiles =
+                    tiles.map {
+                        if (it.tile.tileSpec == spec) {
+                            toggleWidth(it)
+                        } else {
+                            it
+                        }
+                    }
+            }
+        }
+        composeRule.waitForIdle()
+
+        composeRule
+            .onNodeWithContentDescription("tileA")
+            .performCustomAccessibilityActionWithLabel("Toggle size")
+
+        assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun resizedLarge_shouldBeIcon() {
+        var tiles by mutableStateOf(TestEditTiles)
+        val listState = EditTileListState(tiles, 4)
+        composeRule.setContent {
+            EditTileGridUnderTest(listState) { spec ->
+                tiles =
+                    tiles.map {
+                        if (it.tile.tileSpec == spec) {
+                            toggleWidth(it)
+                        } else {
+                            it
+                        }
+                    }
+            }
+        }
+        composeRule.waitForIdle()
+
+        composeRule
+            .onNodeWithContentDescription("tileD_large")
+            .performCustomAccessibilityActionWithLabel("Toggle size")
+
+        assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
+    }
+
+    companion object {
+        private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> {
+            return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1)
+        }
+
+        private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
+            return SizedTileImpl(
+                EditTileViewModel(
+                    tileSpec = TileSpec.create(tileSpec),
+                    icon =
+                        Icon.Resource(
+                            android.R.drawable.star_on,
+                            ContentDescription.Loaded(tileSpec),
+                        ),
+                    label = AnnotatedString(tileSpec),
+                    appName = null,
+                    isCurrent = true,
+                    availableEditActions = emptySet(),
+                    category = TileCategory.UNKNOWN,
+                ),
+                getWidth(tileSpec),
+            )
+        }
+
+        private fun getWidth(tileSpec: String): Int {
+            return if (tileSpec.endsWith("large")) {
+                2
+            } else {
+                1
+            }
+        }
+
+        private val TestEditTiles =
+            listOf(
+                createEditTile("tileA"),
+                createEditTile("tileB"),
+                createEditTile("tileC"),
+                createEditTile("tileD_large"),
+                createEditTile("tileE"),
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 3e7980d..0d39834 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
 
 import static androidx.test.ext.truth.content.IntentSubject.assertThat;
 
@@ -1100,6 +1101,18 @@
     }
 
     @Test
+    public void testNotifsBanned_entryListenerRemove() {
+        mEntryListener.onEntryAdded(mRow);
+        mBubbleController.updateBubble(mBubbleEntry);
+
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Removes the notification
+        mEntryListener.onEntryRemoved(mRow, REASON_PACKAGE_BANNED);
+        assertFalse(mBubbleController.hasBubbles());
+    }
+
+    @Test
     public void removeBubble_intercepted() {
         mEntryListener.onEntryAdded(mRow);
         mBubbleController.updateBubble(mBubbleEntry);
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 765afef..88edb12 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -2169,6 +2169,9 @@
             Log.i(TAG, "callingUid=" + callingUid + ", userId=" + accounts.userId
                     + " performing rename account");
             Account resultingAccount = renameAccountInternal(accounts, accountToRename, newName);
+            if (resultingAccount == null) {
+                resultingAccount = accountToRename;
+            }
             Bundle result = new Bundle();
             result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
             result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type);
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 34c3d7e..a73a991 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -74,6 +74,7 @@
 import android.util.Xml;
 
 import com.android.internal.R;
+import com.android.internal.annotations.Keep;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.util.FrameworkStatsLog;
@@ -214,7 +215,7 @@
 
     class PairingThread extends Thread implements NsdManager.RegistrationListener {
         private NsdManager mNsdManager;
-        private String mPublicKey;
+        @Keep private String mPublicKey;
         private String mPairingCode;
         private String mGuid;
         private String mServiceName;
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index df45a6e..177eefb 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -76,11 +76,12 @@
                     try {
                         mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
                                 intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
-                        dispatchEmergencyStateChanged();
                     } catch (IllegalStateException | UnsupportedOperationException e) {
                         Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
                     }
                 }
+
+                dispatchEmergencyStateChanged();
             }
         }, new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL));
 
@@ -140,9 +141,10 @@
                     if (mIsInEmergencyCall) {
                         mEmergencyCallEndRealtimeMs = SystemClock.elapsedRealtime();
                         mIsInEmergencyCall = false;
-                        dispatchEmergencyStateChanged();
                     }
                 }
+
+                dispatchEmergencyStateChanged();
             }
         }
     }
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index a41675a..6303ecd 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -298,13 +298,12 @@
 
             restoreSettings();
 
-            if (Build.IS_USER) {
-                // Wipe all shell overlays on boot, to recover from a potentially broken device
-                String shellPkgName = TextUtils.emptyIfNull(
-                        getContext().getString(android.R.string.config_systemShell));
-                mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
-                        && shellPkgName.equals(overlayInfo.packageName));
-            }
+            // Wipe all shell overlays on boot, to recover from a potentially broken device
+            String shellPkgName = TextUtils.emptyIfNull(
+                    getContext().getString(android.R.string.config_systemShell));
+            mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
+                    && shellPkgName.equals(overlayInfo.packageName));
+
             initIfNeeded();
             onStartUser(UserHandle.USER_SYSTEM);
 
diff --git a/services/core/java/com/android/server/pm/Android.bp b/services/core/java/com/android/server/pm/Android.bp
new file mode 100644
index 0000000..d625cf2
--- /dev/null
+++ b/services/core/java/com/android/server/pm/Android.bp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package {
+    default_team: "trendy_team_framework_android_packages",
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "framework-pm-service-sources",
+    srcs: [
+        "**/*.java",
+        "**/*.aidl",
+    ],
+    exclude_srcs: [
+        "dex/**/*.java",
+        "User*.java",
+        "Shortcut*.java",
+    ],
+    visibility: ["//frameworks/base"],
+}
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index 2394da9..b84ef37 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -50,6 +50,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.function.IntFunction;
 
 /**
@@ -92,6 +93,12 @@
             new SparseArray<>();
 
     /**
+     * A map from user ID to the active {@link LoadingQueueItem} user when we're loading the launch
+     * params for that user.
+     */
+    private final SparseArray<LoadingQueueItem> mLoadingItemMap = new SparseArray<>();
+
+    /**
      * A map from {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} to
      * activity's component name for reverse queries from window layout affinities to activities.
      * Used to decide if we should use another activity's record with the same affinity.
@@ -117,112 +124,30 @@
     }
 
     void onUnlockUser(int userId) {
-        loadLaunchParams(userId);
+        if (mLoadingItemMap.contains(userId)) {
+            Slog.e(TAG, "Duplicate onUnlockUser " + userId);
+            return;
+        }
+        final LoadingQueueItem item = new LoadingQueueItem(userId);
+        mLoadingItemMap.put(userId, item);
+        mPersisterQueue.addItem(item, /* flush */ false);
     }
 
     void onCleanupUser(int userId) {
+        final LoadingQueueItem item = mLoadingItemMap.removeReturnOld(userId);
+        if (item != null) {
+            item.abort();
+
+            mPersisterQueue.removeItems(
+                    queueItem -> queueItem.mUserId == userId, LoadingQueueItem.class);
+        }
         mLaunchParamsMap.remove(userId);
     }
 
-    private void loadLaunchParams(int userId) {
-        final List<File> filesToDelete = new ArrayList<>();
-        final File launchParamsFolder = getLaunchParamFolder(userId);
-        if (!launchParamsFolder.isDirectory()) {
-            Slog.i(TAG, "Didn't find launch param folder for user " + userId);
-            return;
-        }
-
-        final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
-
-        final File[] paramsFiles = launchParamsFolder.listFiles();
-        final ArrayMap<ComponentName, PersistableLaunchParams> map =
-                new ArrayMap<>(paramsFiles.length);
-        mLaunchParamsMap.put(userId, map);
-
-        for (File paramsFile : paramsFiles) {
-            if (!paramsFile.isFile()) {
-                Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
-                continue;
-            }
-            if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
-                Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
-                filesToDelete.add(paramsFile);
-                continue;
-            }
-            String paramsFileName = paramsFile.getName();
-            // Migrate all records from old separator to new separator.
-            final int oldSeparatorIndex =
-                    paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
-            if (oldSeparatorIndex != -1) {
-                if (paramsFileName.indexOf(
-                        OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
-                    // Rare case. We have more than one old escaped component separator probably
-                    // because this app uses underscore in their package name. We can't distinguish
-                    // which one is the real separator so let's skip it.
-                    filesToDelete.add(paramsFile);
-                    continue;
-                }
-                paramsFileName = paramsFileName.replace(
-                        OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
-                final File newFile = new File(launchParamsFolder, paramsFileName);
-                if (paramsFile.renameTo(newFile)) {
-                    paramsFile = newFile;
-                } else {
-                    // Rare case. For some reason we can't rename the file. Let's drop this record
-                    // instead.
-                    filesToDelete.add(paramsFile);
-                    continue;
-                }
-            }
-            final String componentNameString = paramsFileName.substring(
-                    0 /* beginIndex */,
-                    paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
-                    .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
-            final ComponentName name = ComponentName.unflattenFromString(
-                    componentNameString);
-            if (name == null) {
-                Slog.w(TAG, "Unexpected file name: " + paramsFileName);
-                filesToDelete.add(paramsFile);
-                continue;
-            }
-
-            if (!packages.contains(name.getPackageName())) {
-                // Rare case. PersisterQueue doesn't have a chance to remove files for removed
-                // packages last time.
-                filesToDelete.add(paramsFile);
-                continue;
-            }
-
-            try (InputStream in = new FileInputStream(paramsFile)) {
-                final PersistableLaunchParams params = new PersistableLaunchParams();
-                final TypedXmlPullParser parser = Xml.resolvePullParser(in);
-                int event;
-                while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
-                        && event != XmlPullParser.END_TAG) {
-                    if (event != XmlPullParser.START_TAG) {
-                        continue;
-                    }
-
-                    final String tagName = parser.getName();
-                    if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
-                        Slog.w(TAG, "Unexpected tag name: " + tagName);
-                        continue;
-                    }
-
-                    params.restore(paramsFile, parser);
-                }
-
-                map.put(name, params);
-                addComponentNameToLaunchParamAffinityMapIfNotNull(
-                        name, params.mWindowLayoutAffinity);
-            } catch (Exception e) {
-                Slog.w(TAG, "Failed to restore launch params for " + name, e);
-                filesToDelete.add(paramsFile);
-            }
-        }
-
-        if (!filesToDelete.isEmpty()) {
-            mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
+    private void waitForLoading(int userId) {
+        final LoadingQueueItem item = mLoadingItemMap.get(userId);
+        if (item != null) {
+            item.waitUntilFinish();
         }
     }
 
@@ -236,6 +161,7 @@
             return;
         }
         final int userId = task.mUserId;
+        waitForLoading(userId);
         PersistableLaunchParams params;
         ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId);
         if (map == null) {
@@ -297,6 +223,7 @@
     void getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams) {
         final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
         final int userId = task != null ? task.mUserId : activity.mUserId;
+        waitForLoading(userId);
         final String windowLayoutAffinity;
         if (task != null) {
             windowLayoutAffinity = task.mWindowLayoutAffinity;
@@ -394,6 +321,156 @@
         }
     }
 
+    /**
+     * The work item used to load launch parameters with {@link PersisterQueue} in a background
+     * thread, so that we don't block the thread {@link com.android.server.am.UserController} uses
+     * to broadcast user state changes for I/O operations. See b/365983567 for more details.
+     */
+    private class LoadingQueueItem implements PersisterQueue.QueueItem {
+        private final int mUserId;
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private boolean mAborted = false;
+
+        private LoadingQueueItem(int userId) {
+            mUserId = userId;
+        }
+
+        @Override
+        public void process() {
+            try {
+                loadLaunchParams();
+            } finally {
+                synchronized (mSupervisor.mService.getGlobalLock()) {
+                    mLoadingItemMap.remove(mUserId);
+                    mLatch.countDown();
+                }
+            }
+        }
+
+        private void abort() {
+            mAborted = true;
+        }
+
+        private void waitUntilFinish() {
+            if (mAborted) {
+                return;
+            }
+
+            try {
+                mLatch.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        private void loadLaunchParams() {
+            final List<File> filesToDelete = new ArrayList<>();
+            final File launchParamsFolder = getLaunchParamFolder(mUserId);
+            if (!launchParamsFolder.isDirectory()) {
+                Slog.i(TAG, "Didn't find launch param folder for user " + mUserId);
+                return;
+            }
+
+            final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
+
+            final File[] paramsFiles = launchParamsFolder.listFiles();
+            final ArrayMap<ComponentName, PersistableLaunchParams> map =
+                    new ArrayMap<>(paramsFiles.length);
+
+            for (File paramsFile : paramsFiles) {
+                if (!paramsFile.isFile()) {
+                    Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
+                    continue;
+                }
+                if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
+                    Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
+                    filesToDelete.add(paramsFile);
+                    continue;
+                }
+                String paramsFileName = paramsFile.getName();
+                // Migrate all records from old separator to new separator.
+                final int oldSeparatorIndex =
+                        paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
+                if (oldSeparatorIndex != -1) {
+                    if (paramsFileName.indexOf(
+                            OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
+                        // Rare case. We have more than one old escaped component separator probably
+                        // because this app uses underscore in their package name. We can't
+                        // distinguish which one is the real separator so let's skip it.
+                        filesToDelete.add(paramsFile);
+                        continue;
+                    }
+                    paramsFileName = paramsFileName.replace(
+                            OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
+                    final File newFile = new File(launchParamsFolder, paramsFileName);
+                    if (paramsFile.renameTo(newFile)) {
+                        paramsFile = newFile;
+                    } else {
+                        // Rare case. For some reason we can't rename the file. Let's drop this
+                        // record instead.
+                        filesToDelete.add(paramsFile);
+                        continue;
+                    }
+                }
+                final String componentNameString = paramsFileName.substring(
+                                0 /* beginIndex */,
+                                paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
+                        .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
+                final ComponentName name = ComponentName.unflattenFromString(
+                        componentNameString);
+                if (name == null) {
+                    Slog.w(TAG, "Unexpected file name: " + paramsFileName);
+                    filesToDelete.add(paramsFile);
+                    continue;
+                }
+
+                if (!packages.contains(name.getPackageName())) {
+                    // Rare case. PersisterQueue doesn't have a chance to remove files for removed
+                    // packages last time.
+                    filesToDelete.add(paramsFile);
+                    continue;
+                }
+
+                try (InputStream in = new FileInputStream(paramsFile)) {
+                    final PersistableLaunchParams params = new PersistableLaunchParams();
+                    final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+                    int event;
+                    while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
+                            && event != XmlPullParser.END_TAG) {
+                        if (event != XmlPullParser.START_TAG) {
+                            continue;
+                        }
+
+                        final String tagName = parser.getName();
+                        if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
+                            Slog.w(TAG, "Unexpected tag name: " + tagName);
+                            continue;
+                        }
+
+                        params.restore(paramsFile, parser);
+                    }
+
+                    map.put(name, params);
+                    addComponentNameToLaunchParamAffinityMapIfNotNull(
+                            name, params.mWindowLayoutAffinity);
+                } catch (Exception e) {
+                    Slog.w(TAG, "Failed to restore launch params for " + name, e);
+                    filesToDelete.add(paramsFile);
+                }
+            }
+
+            synchronized (mSupervisor.mService.getGlobalLock()) {
+                if (!mAborted) {
+                    mLaunchParamsMap.put(mUserId, map);
+                }
+            }
+
+            if (!filesToDelete.isEmpty()) {
+                mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
+            }
+        }
+    }
+
     private class LaunchParamsWriteQueueItem
             implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
         private final int mUserId;
@@ -466,7 +543,8 @@
         }
     }
 
-    private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
+    private static class CleanUpComponentQueueItem
+            implements PersisterQueue.WriteQueueItem<CleanUpComponentQueueItem> {
         private final List<File> mComponentFiles;
 
         private CleanUpComponentQueueItem(List<File> componentFiles) {
@@ -483,7 +561,7 @@
         }
     }
 
-    private class PersistableLaunchParams {
+    private static class PersistableLaunchParams {
         private static final String ATTR_WINDOWING_MODE = "windowing_mode";
         private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
         private static final String ATTR_BOUNDS = "bounds";
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 9dc3d6a..f66069c 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -49,14 +49,16 @@
     /** Special value for mWriteTime to mean don't wait, just write */
     private static final long FLUSH_QUEUE = -1;
 
-    /** An {@link WriteQueueItem} that doesn't do anything. Used to trigger {@link
-     * Listener#onPreProcessItem}. */
-    static final WriteQueueItem EMPTY_ITEM = () -> { };
+    /**
+     * A {@link QueueItem} that doesn't do anything. Used to trigger
+     * {@link Listener#onPreProcessItem}.
+     */
+    static final QueueItem EMPTY_ITEM = () -> { };
 
     private final long mInterWriteDelayMs;
     private final long mPreTaskDelayMs;
     private final LazyTaskWriterThread mLazyTaskWriterThread;
-    private final ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<>();
+    private final ArrayList<QueueItem> mQueue = new ArrayList<>();
 
     private final ArrayList<Listener> mListeners = new ArrayList<>();
 
@@ -105,10 +107,10 @@
         mLazyTaskWriterThread.join();
     }
 
-    synchronized void addItem(WriteQueueItem item, boolean flush) {
-        mWriteQueue.add(item);
+    synchronized void addItem(QueueItem item, boolean flush) {
+        mQueue.add(item);
 
-        if (flush || mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
+        if (flush || mQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
             mNextWriteTime = FLUSH_QUEUE;
         } else if (mNextWriteTime == 0) {
             mNextWriteTime = SystemClock.uptimeMillis() + mPreTaskDelayMs;
@@ -116,11 +118,12 @@
         notify();
     }
 
-    synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate, Class<T> clazz) {
-        for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
-            WriteQueueItem writeQueueItem = mWriteQueue.get(i);
-            if (clazz.isInstance(writeQueueItem)) {
-                T item = clazz.cast(writeQueueItem);
+    synchronized <T extends WriteQueueItem<T>> T findLastItem(Predicate<T> predicate,
+            Class<T> clazz) {
+        for (int i = mQueue.size() - 1; i >= 0; --i) {
+            QueueItem queueItem = mQueue.get(i);
+            if (clazz.isInstance(queueItem)) {
+                T item = clazz.cast(queueItem);
                 if (predicate.test(item)) {
                     return item;
                 }
@@ -134,7 +137,7 @@
      * Updates the last item found in the queue that matches the given item, or adds it to the end
      * of the queue if no such item is found.
      */
-    synchronized <T extends WriteQueueItem> void updateLastOrAddItem(T item, boolean flush) {
+    synchronized <T extends WriteQueueItem<T>> void updateLastOrAddItem(T item, boolean flush) {
         final T itemToUpdate = findLastItem(item::matches, (Class<T>) item.getClass());
         if (itemToUpdate == null) {
             addItem(item, flush);
@@ -148,15 +151,15 @@
     /**
      * Removes all items with which given predicate returns {@code true}.
      */
-    synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
+    synchronized <T extends QueueItem> void removeItems(Predicate<T> predicate,
             Class<T> clazz) {
-        for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
-            WriteQueueItem writeQueueItem = mWriteQueue.get(i);
-            if (clazz.isInstance(writeQueueItem)) {
-                T item = clazz.cast(writeQueueItem);
+        for (int i = mQueue.size() - 1; i >= 0; --i) {
+            QueueItem queueItem = mQueue.get(i);
+            if (clazz.isInstance(queueItem)) {
+                T item = clazz.cast(queueItem);
                 if (predicate.test(item)) {
                     if (DEBUG) Slog.d(TAG, "Removing " + item + " from write queue.");
-                    mWriteQueue.remove(i);
+                    mQueue.remove(i);
                 }
             }
         }
@@ -201,7 +204,7 @@
         // See https://b.corp.google.com/issues/64438652#comment7
 
         // If mNextWriteTime, then don't delay between each call to saveToXml().
-        final WriteQueueItem item;
+        final QueueItem item;
         synchronized (this) {
             if (mNextWriteTime != FLUSH_QUEUE) {
                 // The next write we don't have to wait so long.
@@ -212,7 +215,7 @@
                 }
             }
 
-            while (mWriteQueue.isEmpty()) {
+            while (mQueue.isEmpty()) {
                 if (mNextWriteTime != 0) {
                     mNextWriteTime = 0; // idle.
                     notify(); // May need to wake up flush().
@@ -224,17 +227,18 @@
                 }
                 if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
                 wait();
-                // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_WRITE_DELAY_MS
+                // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_TASK_DELAY_MS
                 // from now.
             }
-            item = mWriteQueue.remove(0);
+            item = mQueue.remove(0);
 
+            final boolean isWriteItem = item instanceof WriteQueueItem<?>;
             long now = SystemClock.uptimeMillis();
             if (DEBUG) {
                 Slog.d(TAG, "LazyTaskWriter: now=" + now + " mNextWriteTime=" + mNextWriteTime
-                        + " mWriteQueue.size=" + mWriteQueue.size());
+                        + " mWriteQueue.size=" + mQueue.size() + " isWriteItem=" + isWriteItem);
             }
-            while (now < mNextWriteTime) {
+            while (now < mNextWriteTime && isWriteItem) {
                 if (DEBUG) {
                     Slog.d(TAG, "LazyTaskWriter: waiting " + (mNextWriteTime - now));
                 }
@@ -248,9 +252,18 @@
         item.process();
     }
 
-    interface WriteQueueItem<T extends WriteQueueItem<T>> {
+    /**
+     * An item the {@link PersisterQueue} processes. Used for loading tasks. Subclasses of this, but
+     * not {@link WriteQueueItem}, aren't subject to waiting.
+     */
+    interface QueueItem {
         void process();
+    }
 
+    /**
+     * A write item the {@link PersisterQueue} processes. Used for persisting tasks.
+     */
+    interface WriteQueueItem<T extends WriteQueueItem<T>> extends QueueItem {
         default void updateFrom(T item) {}
 
         default boolean matches(T item) {
@@ -288,7 +301,7 @@
                 while (true) {
                     final boolean probablyDone;
                     synchronized (PersisterQueue.this) {
-                        probablyDone = mWriteQueue.isEmpty();
+                        probablyDone = mQueue.isEmpty();
                     }
 
                     for (int i = mListeners.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 655a6fb..0a47522 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2925,6 +2925,9 @@
             final TaskFragment taskFragment = target.asTaskFragment();
             final boolean isEmbeddedTaskFragment = taskFragment != null
                     && taskFragment.isEmbedded();
+            final IBinder taskFragmentToken =
+                    taskFragment != null ? taskFragment.getFragmentToken() : null;
+            change.setTaskFragmentToken(taskFragmentToken);
             final ActivityRecord activityRecord = target.asActivityRecord();
 
             if (task != null) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9fdf088..3b334ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -436,6 +436,10 @@
     private static final String PROFILING_SERVICE_JAR_PATH =
             "/apex/com.android.profiling/javalib/service-profiling.jar";
 
+    private static final String RANGING_APEX_SERVICE_JAR_PATH =
+            "/apex/com.android.uwb/javalib/service-ranging.jar";
+    private static final String RANGING_SERVICE_CLASS = "com.android.server.ranging.RangingService";
+
     private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector";
 
     private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst";
@@ -3015,6 +3019,17 @@
             t.traceEnd();
         }
 
+        if (com.android.ranging.flags.Flags.rangingStackEnabled()) {
+            if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)
+                    || context.getPackageManager().hasSystemFeature(
+                            PackageManager.FEATURE_WIFI_RTT)) {
+                t.traceBegin("RangingService");
+                mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS,
+                        RANGING_APEX_SERVICE_JAR_PATH);
+                t.traceEnd();
+            }
+        }
+
         t.traceBegin("StartBootPhaseDeviceSpecificServicesReady");
         mSystemServiceManager.startBootPhase(t, SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY);
         t.traceEnd();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
index 1be61c3..62d3949 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -293,6 +293,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
@@ -311,6 +312,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
         target.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
@@ -339,6 +341,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
 
@@ -408,6 +411,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
@@ -425,6 +429,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
@@ -453,6 +458,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
@@ -470,6 +476,7 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
@@ -488,12 +495,52 @@
                 mUserFolderGetter);
         target.onSystemReady();
         target.onUnlockUser(TEST_USER_ID);
+        mPersisterQueue.flush();
 
         target.getLaunchParams(mTestTask, null, mResult);
 
         assertTrue("Result should be empty.", mResult.isEmpty());
     }
 
+    @Test
+    public void testAbortsLoadingWhenUserCleansUpBeforeLoadingFinishes() {
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+        assertEquals(1, mPersisterQueue.mQueue.size());
+        PersisterQueue.QueueItem item = mPersisterQueue.mQueue.get(0);
+
+        target.onCleanupUser(TEST_USER_ID);
+        mPersisterQueue.flush();
+
+        // Explicitly run the loading item to mimic the situation where the item already started.
+        item.process();
+
+        target.getLaunchParams(mTestTask, null, mResult);
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testGetLaunchParamsNotBlockedByAbortedLoading() {
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+        target.onCleanupUser(TEST_USER_ID);
+
+        // As long as the call in the next line returns, we know it's not waiting for the loading to
+        // finish because we run items synchronously in this test.
+        target.getLaunchParams(mTestTask, null, mResult);
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
     private static boolean deleteRecursively(File file) {
         boolean result = true;
         if (file.isDirectory()) {
@@ -508,17 +555,17 @@
 
     /**
      * Test double to {@link PersisterQueue}. This is not thread-safe and caller should always use
-     * {@link #flush()} to execute write items in it.
+     * {@link #flush()} to execute items in it.
      */
     static class TestPersisterQueue extends PersisterQueue {
-        private List<WriteQueueItem> mWriteQueue = new ArrayList<>();
+        private List<QueueItem> mQueue = new ArrayList<>();
         private List<Listener> mListeners = new ArrayList<>();
 
         @Override
         void flush() {
-            while (!mWriteQueue.isEmpty()) {
-                final WriteQueueItem item = mWriteQueue.remove(0);
-                final boolean queueEmpty = mWriteQueue.isEmpty();
+            while (!mQueue.isEmpty()) {
+                final QueueItem item = mQueue.remove(0);
+                final boolean queueEmpty = mQueue.isEmpty();
                 for (Listener listener : mListeners) {
                     listener.onPreProcessItem(queueEmpty);
                 }
@@ -537,18 +584,18 @@
         }
 
         @Override
-        void addItem(WriteQueueItem item, boolean flush) {
-            mWriteQueue.add(item);
+        synchronized void addItem(QueueItem item, boolean flush) {
+            mQueue.add(item);
             if (flush) {
                 flush();
             }
         }
 
         @Override
-        synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate,
+        synchronized <T extends WriteQueueItem<T>> T findLastItem(Predicate<T> predicate,
                 Class<T> clazz) {
-            for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
-                WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+            for (int i = mQueue.size() - 1; i >= 0; --i) {
+                QueueItem writeQueueItem = mQueue.get(i);
                 if (clazz.isInstance(writeQueueItem)) {
                     T item = clazz.cast(writeQueueItem);
                     if (predicate.test(item)) {
@@ -561,14 +608,14 @@
         }
 
         @Override
-        synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
+        synchronized <T extends QueueItem> void removeItems(Predicate<T> predicate,
                 Class<T> clazz) {
-            for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
-                WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+            for (int i = mQueue.size() - 1; i >= 0; --i) {
+                QueueItem writeQueueItem = mQueue.get(i);
                 if (clazz.isInstance(writeQueueItem)) {
                     T item = clazz.cast(writeQueueItem);
                     if (predicate.test(item)) {
-                        mWriteQueue.remove(i);
+                        mQueue.remove(i);
                     }
                 }
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 3e87f1f..ce0e6f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -90,9 +90,27 @@
         mFactory.setExpectedProcessedItemNumber(1);
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
 
-        final long dispatchTime = SystemClock.uptimeMillis();
         mTarget.addItem(mFactory.createItem(), false);
         assertTrue("Target didn't process item enough times.",
+                mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
+        assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
+
+        assertTrue("Target didn't call callback enough times.",
+                mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
+        // Once before processing this item, once after that.
+        assertEquals(2, mListener.mProbablyDoneResults.size());
+        // The last one must be called with probably done being true.
+        assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(1));
+    }
+
+    @Test
+    public void testProcessOneWriteItem() throws Exception {
+        mFactory.setExpectedProcessedItemNumber(1);
+        mListener.setExpectedOnPreProcessItemCallbackTimes(1);
+
+        final long dispatchTime = SystemClock.uptimeMillis();
+        mTarget.addItem(mFactory.createWriteItem(), false);
+        assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
         assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
         final long processDuration = SystemClock.uptimeMillis() - dispatchTime;
@@ -109,12 +127,12 @@
     }
 
     @Test
-    public void testProcessOneItem_Flush() throws Exception {
+    public void testProcessOneWriteItem_Flush() throws Exception {
         mFactory.setExpectedProcessedItemNumber(1);
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
 
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(mFactory.createItem(), true);
+        mTarget.addItem(mFactory.createWriteItem(), true);
         assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
         assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
@@ -138,8 +156,8 @@
         mListener.setExpectedOnPreProcessItemCallbackTimes(2);
 
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(mFactory.createItem(), false);
-        mTarget.addItem(mFactory.createItem(), false);
+        mTarget.addItem(mFactory.createWriteItem(), false);
+        mTarget.addItem(mFactory.createWriteItem(), false);
         assertTrue("Target didn't call callback enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS
                         + TIMEOUT_ALLOWANCE));
@@ -165,7 +183,7 @@
         mFactory.setExpectedProcessedItemNumber(1);
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(mFactory.createItem(), false);
+        mTarget.addItem(mFactory.createWriteItem(), false);
         assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
         long processDuration = SystemClock.uptimeMillis() - dispatchTime;
@@ -184,7 +202,7 @@
         // Synchronize on the instance to make sure we schedule the item after it starts to wait for
         // task indefinitely.
         synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
+            mTarget.addItem(mFactory.createWriteItem(), false);
         }
         assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -206,9 +224,9 @@
     @Test
     public void testFindLastItemNotReturnDifferentType() {
         synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
-            assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
-                    FilterableTestItem.class));
+            mTarget.addItem(mFactory.createWriteItem(), false);
+            assertNull(mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+                    FilterableTestWriteItem.class));
         }
     }
 
@@ -216,18 +234,18 @@
     public void testFindLastItemNotReturnMismatchItem() {
         synchronized (mTarget) {
             mTarget.addItem(mFactory.createFilterableItem(false), false);
-            assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
-                    FilterableTestItem.class));
+            assertNull(mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+                    FilterableTestWriteItem.class));
         }
     }
 
     @Test
     public void testFindLastItemReturnMatchedItem() {
         synchronized (mTarget) {
-            final FilterableTestItem item = mFactory.createFilterableItem(true);
+            final FilterableTestWriteItem item = mFactory.createFilterableItem(true);
             mTarget.addItem(item, false);
-            assertSame(item, mTarget.findLastItem(TestItem::shouldKeepOnFilter,
-                    FilterableTestItem.class));
+            assertSame(item, mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+                    FilterableTestWriteItem.class));
         }
     }
 
@@ -235,8 +253,8 @@
     public void testRemoveItemsNotRemoveDifferentType() throws Exception {
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
-            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+            mTarget.addItem(mFactory.createWriteItem(), false);
+            mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
                 mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -248,7 +266,7 @@
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
             mTarget.addItem(mFactory.createFilterableItem(false), false);
-            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+            mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
                 mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -258,8 +276,8 @@
     @Test
     public void testUpdateLastOrAddItemUpdatesMatchedItem() throws Exception {
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
-        final FilterableTestItem scheduledItem = mFactory.createFilterableItem(true);
-        final FilterableTestItem expected = mFactory.createFilterableItem(true);
+        final FilterableTestWriteItem scheduledItem = mFactory.createFilterableItem(true);
+        final FilterableTestWriteItem expected = mFactory.createFilterableItem(true);
         synchronized (mTarget) {
             mTarget.addItem(scheduledItem, false);
             mTarget.updateLastOrAddItem(expected, false);
@@ -274,8 +292,8 @@
     @Test
     public void testUpdateLastOrAddItemUpdatesAddItemWhenNoMatch() throws Exception {
         mListener.setExpectedOnPreProcessItemCallbackTimes(2);
-        final FilterableTestItem scheduledItem = mFactory.createFilterableItem(false);
-        final FilterableTestItem expected = mFactory.createFilterableItem(true);
+        final FilterableTestWriteItem scheduledItem = mFactory.createFilterableItem(false);
+        final FilterableTestWriteItem expected = mFactory.createFilterableItem(true);
         synchronized (mTarget) {
             mTarget.addItem(scheduledItem, false);
             mTarget.updateLastOrAddItem(expected, false);
@@ -292,9 +310,9 @@
     public void testRemoveItemsRemoveMatchedItem() throws Exception {
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
+            mTarget.addItem(mFactory.createWriteItem(), false);
             mTarget.addItem(mFactory.createFilterableItem(true), false);
-            mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+            mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
         }
         assertTrue("Target didn't call callback enough times.",
                 mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -304,8 +322,8 @@
     @Test
     public void testFlushWaitSynchronously() {
         final long dispatchTime = SystemClock.uptimeMillis();
-        mTarget.addItem(mFactory.createItem(), false);
-        mTarget.addItem(mFactory.createItem(), false);
+        mTarget.addItem(mFactory.createWriteItem(), false);
+        mTarget.addItem(mFactory.createWriteItem(), false);
         mTarget.flush();
         assertEquals("Flush should wait until all items are processed before return.",
                 2, mFactory.getTotalProcessedItemCount());
@@ -335,15 +353,18 @@
             return new TestItem(mItemCount, mLatch);
         }
 
-        FilterableTestItem createFilterableItem(boolean shouldKeepOnFilter) {
-            return new FilterableTestItem(shouldKeepOnFilter, mItemCount, mLatch);
+        TestWriteItem createWriteItem() {
+            return new TestWriteItem(mItemCount, mLatch);
+        }
+
+        FilterableTestWriteItem createFilterableItem(boolean shouldKeepOnFilter) {
+            return new FilterableTestWriteItem(shouldKeepOnFilter, mItemCount, mLatch);
         }
     }
 
-    private static class TestItem<T extends TestItem<T>>
-            implements PersisterQueue.WriteQueueItem<T> {
-        private AtomicInteger mItemCount;
-        private CountDownLatch mLatch;
+    private static class TestItem implements PersisterQueue.QueueItem {
+        private final AtomicInteger mItemCount;
+        private final CountDownLatch mLatch;
 
         TestItem(AtomicInteger itemCount, CountDownLatch latch) {
             mItemCount = itemCount;
@@ -359,30 +380,37 @@
                 mLatch.countDown();
             }
         }
+    }
+
+    private static class TestWriteItem<T extends TestWriteItem<T>>
+            extends TestItem implements PersisterQueue.WriteQueueItem<T> {
+        TestWriteItem(AtomicInteger itemCount, CountDownLatch latch) {
+            super(itemCount, latch);
+        }
 
         boolean shouldKeepOnFilter() {
             return true;
         }
     }
 
-    private static class FilterableTestItem extends TestItem<FilterableTestItem> {
+    private static class FilterableTestWriteItem extends TestWriteItem<FilterableTestWriteItem> {
         private boolean mShouldKeepOnFilter;
 
-        private FilterableTestItem mUpdateFromItem;
+        private FilterableTestWriteItem mUpdateFromItem;
 
-        private FilterableTestItem(boolean shouldKeepOnFilter, AtomicInteger mItemCount,
+        private FilterableTestWriteItem(boolean shouldKeepOnFilter, AtomicInteger mItemCount,
                 CountDownLatch mLatch) {
             super(mItemCount, mLatch);
             mShouldKeepOnFilter = shouldKeepOnFilter;
         }
 
         @Override
-        public boolean matches(FilterableTestItem item) {
+        public boolean matches(FilterableTestWriteItem item) {
             return item.mShouldKeepOnFilter;
         }
 
         @Override
-        public void updateFrom(FilterableTestItem item) {
+        public void updateFrom(FilterableTestWriteItem item) {
             mUpdateFromItem = item;
         }
 
diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java
new file mode 100644
index 0000000..501fd65
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.hosttest.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
+ * QUESTIONS ABOUT IT.
+ *
+ * @hide
+ */
+@Target({METHOD, CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface HostSideTestIgnore {
+}
diff --git a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
index eba8e62..001943c 100644
--- a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
+++ b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
@@ -24,6 +24,9 @@
 --remove-annotation
     android.hosttest.annotation.HostSideTestRemove
 
+--ignore-annotation
+    android.hosttest.annotation.HostSideTestIgnore
+
 --substitute-annotation
     android.hosttest.annotation.HostSideTestSubstitute
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 34aaaa9..165bb57 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -166,6 +166,7 @@
             options.keepClassAnnotations,
             options.throwAnnotations,
             options.removeAnnotations,
+            options.ignoreAnnotations,
             options.substituteAnnotations,
             options.redirectAnnotations,
             options.redirectionClassAnnotations,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 057a52c..b083d89 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -84,6 +84,7 @@
         var keepAnnotations: MutableSet<String> = mutableSetOf(),
         var throwAnnotations: MutableSet<String> = mutableSetOf(),
         var removeAnnotations: MutableSet<String> = mutableSetOf(),
+        var ignoreAnnotations: MutableSet<String> = mutableSetOf(),
         var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
         var redirectAnnotations: MutableSet<String> = mutableSetOf(),
 
@@ -184,6 +185,9 @@
                         "--remove-annotation" ->
                             ret.removeAnnotations.addUniqueAnnotationArg()
 
+                        "--ignore-annotation" ->
+                            ret.ignoreAnnotations.addUniqueAnnotationArg()
+
                         "--substitute-annotation" ->
                             ret.substituteAnnotations.addUniqueAnnotationArg()
 
@@ -277,6 +281,7 @@
               keepAnnotations=$keepAnnotations,
               throwAnnotations=$throwAnnotations,
               removeAnnotations=$removeAnnotations,
+              ignoreAnnotations=$ignoreAnnotations,
               keepClassAnnotations=$keepClassAnnotations,
               substituteAnnotations=$substituteAnnotations,
               nativeSubstituteAnnotations=$redirectionClassAnnotations,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index a6b8cdb..36adf06 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -48,6 +48,7 @@
     keepClassAnnotations_: Set<String>,
     throwAnnotations_: Set<String>,
     removeAnnotations_: Set<String>,
+    ignoreAnnotations_: Set<String>,
     substituteAnnotations_: Set<String>,
     redirectAnnotations_: Set<String>,
     redirectionClassAnnotations_: Set<String>,
@@ -60,6 +61,7 @@
     private val keepClassAnnotations = convertToInternalNames(keepClassAnnotations_)
     private val throwAnnotations = convertToInternalNames(throwAnnotations_)
     private val removeAnnotations = convertToInternalNames(removeAnnotations_)
+    private val ignoreAnnotations = convertToInternalNames(ignoreAnnotations_)
     private val redirectAnnotations = convertToInternalNames(redirectAnnotations_)
     private val substituteAnnotations = convertToInternalNames(substituteAnnotations_)
     private val redirectionClassAnnotations =
@@ -73,6 +75,7 @@
             keepClassAnnotations +
             throwAnnotations +
             removeAnnotations +
+            ignoreAnnotations +
             redirectAnnotations +
             substituteAnnotations
 
@@ -107,6 +110,7 @@
             in substituteAnnotations -> FilterPolicy.Substitute.withReason(REASON_ANNOTATION)
             in throwAnnotations -> FilterPolicy.Throw.withReason(REASON_ANNOTATION)
             in removeAnnotations -> FilterPolicy.Remove.withReason(REASON_ANNOTATION)
+            in ignoreAnnotations -> FilterPolicy.Ignore.withReason(REASON_ANNOTATION)
             in redirectAnnotations -> FilterPolicy.Redirect.withReason(REASON_ANNOTATION)
             else -> null
         }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
index 7440b94..d6aa761 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
@@ -27,16 +27,22 @@
 class ClassFilter private constructor(
     private val defaultResult: Boolean,
 ) {
+    private enum class MatchType {
+        Full,
+        Prefix,
+        Suffix,
+    }
+
     private class FilterElement(
         val allowed: Boolean,
         val internalName: String,
-        val isPrefix: Boolean,
+        val matchType: MatchType,
     ) {
         fun matches(classInternalName: String): Boolean {
-            return if (isPrefix) {
-                classInternalName.startsWith(internalName)
-            } else {
-                classInternalName == internalName
+            return when (matchType) {
+                MatchType.Full -> classInternalName == internalName
+                MatchType.Prefix -> classInternalName.startsWith(internalName)
+                MatchType.Suffix -> classInternalName.endsWith(internalName)
             }
         }
     }
@@ -114,15 +120,29 @@
 
                 // Special case -- matches any class names.
                 if (line == "*") {
-                    ret.elements.add(FilterElement(allow, "", true))
+                    ret.elements.add(FilterElement(allow, "", MatchType.Prefix))
                     return@forEach
                 }
 
-                // Handle wildcard -- e.g. "package.name.*"
+                // Handle prefix match -- e.g. "package.name.*"
                 if (line.endsWith(".*")) {
                     ret.elements.add(
                         FilterElement(
-                            allow, line.substring(0, line.length - 2).toJvmClassName(), true
+                            allow,
+                            line.substring(0, line.length - 2).toJvmClassName() + "/",
+                            MatchType.Prefix
+                        )
+                    )
+                    return@forEach
+                }
+
+                // Handle suffix match -- e.g. "*.Flags"
+                if (line.startsWith("*.")) {
+                    ret.elements.add(
+                        FilterElement(
+                            allow,
+                            "/" + line.substring(2, line.length).toJvmClassName(),
+                            MatchType.Suffix
                         )
                     )
                     return@forEach
@@ -136,10 +156,10 @@
                         lineNo
                     )
                 }
-                ret.elements.add(FilterElement(allow, line.toJvmClassName(), false))
+                ret.elements.add(FilterElement(allow, line.toJvmClassName(), MatchType.Suffix))
             }
 
             return ret
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 82586bb..103e152 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -21,6 +21,26 @@
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
+## Class: android/hosttest/annotation/HostSideTestIgnore.class
+  Compiled from "HostSideTestIgnore.java"
+public interface android.hosttest.annotation.HostSideTestIgnore extends java.lang.annotation.Annotation
+  minor version: 0
+  major version: 61
+  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+  this_class: #x                          // android/hosttest/annotation/HostSideTestIgnore
+  super_class: #x                         // java/lang/Object
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestIgnore.java"
+RuntimeVisibleAnnotations:
+  x: #x(#x=[e#x.#x,e#x.#x])
+    java.lang.annotation.Target(
+      value=[Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
+    )
+  x: #x(#x=e#x.#x)
+    java.lang.annotation.Retention(
+      value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+    )
 ## Class: android/hosttest/annotation/HostSideTestKeep.class
   Compiled from "HostSideTestKeep.java"
 public interface android.hosttest.annotation.HostSideTestKeep extends java.lang.annotation.Annotation
@@ -382,7 +402,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 2, methods: 8, attributes: 2
+  interfaces: 0, fields: 2, methods: 9, attributes: 2
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -522,6 +542,24 @@
     RuntimeInvisibleAnnotations:
       x: #x()
         android.hosttest.annotation.HostSideTestThrow
+
+  public int toBeIgnored();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String not supported on host side
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      10     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations;
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestIgnore
 }
 SourceFile: "TinyFrameworkAnnotations.java"
 RuntimeInvisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
index 31bbcc5..eeec554 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
@@ -432,7 +432,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 1, methods: 6, attributes: 3
+  interfaces: 0, fields: 1, methods: 7, attributes: 3
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -554,6 +554,22 @@
     RuntimeInvisibleAnnotations:
       x: #x()
         android.hosttest.annotation.HostSideTestThrow
+
+  public int toBeIgnored();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         x: iconst_0
+         x: ireturn
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsIgnore
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestIgnore
 }
 SourceFile: "TinyFrameworkAnnotations.java"
 RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 41f459a..0f8af92 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -593,7 +593,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 1, methods: 6, attributes: 3
+  interfaces: 0, fields: 1, methods: 7, attributes: 3
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -743,6 +743,27 @@
     RuntimeInvisibleAnnotations:
       x: #x()
         android.hosttest.annotation.HostSideTestThrow
+
+  public int toBeIgnored();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
+         x: ldc           #x                 // String toBeIgnored
+         x: ldc           #x                 // String ()I
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: iconst_0
+        x: ireturn
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsIgnore
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestIgnore
 }
 SourceFile: "TinyFrameworkAnnotations.java"
 RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
index ed0fa26..3415deb 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
@@ -16,6 +16,7 @@
 package com.android.hoststubgen.test.tinyframework;
 
 import android.hosttest.annotation.HostSideTestClassLoadHook;
+import android.hosttest.annotation.HostSideTestIgnore;
 import android.hosttest.annotation.HostSideTestKeep;
 import android.hosttest.annotation.HostSideTestRemove;
 import android.hosttest.annotation.HostSideTestSubstitute;
@@ -71,4 +72,9 @@
     public String unsupportedMethod() {
         return "This value shouldn't be seen on the host side.";
     }
+
+    @HostSideTestIgnore
+    public int toBeIgnored() {
+        throw new RuntimeException("not supported on host side");
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
index 34c98e9..1816b38 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
@@ -102,4 +102,11 @@
         assertThat(new TinyFrameworkNestedClasses.StaticNestedClass.Double$NestedClass().value)
                 .isEqualTo(8);
     }
+
+    @Test
+    public void testIgnoreAnnotation() {
+        // The actual method will throw, but because of @Ignore, it'll return 0.
+        assertThat(new TinyFrameworkAnnotations().toBeIgnored())
+                .isEqualTo(0);
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
index 85b6e80..d4e75d4 100644
--- a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
+++ b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
@@ -69,6 +69,8 @@
         assertThat(f.matches("d/e/f")).isEqualTo(false)
         assertThat(f.matches("d/e/f/g")).isEqualTo(true)
         assertThat(f.matches("x")).isEqualTo(true)
+
+        assertThat(f.matches("ab/x")).isEqualTo(true)
     }
 
     @Test
@@ -96,4 +98,18 @@
             assertThat(e.message).contains("line 1")
         }
     }
+
+    @Test
+    fun testSuffix() {
+        val f = ClassFilter.buildFromString("""
+            *.Abc       # allow
+            !*          # Disallow by default
+            """.trimIndent(), true, "X")
+        assertThat(f.matches("a/b/c")).isEqualTo(false)
+        assertThat(f.matches("a/Abc")).isEqualTo(true)
+        assertThat(f.matches("a/b/c/Abc")).isEqualTo(true)
+        assertThat(f.matches("a/b/c/Abc\$Nested")).isEqualTo(true)
+
+        assertThat(f.matches("a/XyzAbc")).isEqualTo(false)
+    }
 }
\ No newline at end of file